mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-17 04:05:00 +00:00
Compare commits
26 Commits
testing202
...
v4.4.2
Author | SHA1 | Date | |
---|---|---|---|
ab98f3a102
|
|||
7516e68d77 | |||
7b60b7a8af | |||
d984dec7db
|
|||
46a4dedab8 | |||
db98519e65 | |||
c39637180a
|
|||
15f9409bc8 | |||
5b90d23367 | |||
c48625d1cd | |||
1195b54a68 | |||
2a280b814f | |||
230c758255 | |||
eafda987ae | |||
7db8a371fc | |||
0d0649dd31 | |||
ac12b8cdcf | |||
9c1611d052 | |||
90e3043c3d | |||
af13bf9088 | |||
4aa65d69c7 | |||
9e33aec594 | |||
f88bc7e9f0 | |||
8e78c41549
|
|||
dfab223391 | |||
539752485c |
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
|
8
.changes/v4.4.0.md
Normal file
8
.changes/v4.4.0.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## v4.4.0 - 2025-09-11
|
||||
### Feature
|
||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
|
||||
* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation
|
||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works
|
||||
### Fixed
|
||||
* Fix display of 'duplicate' and 'merge' buttons in CRUD templates
|
||||
* Fix saving notification preferences in user's profile
|
3
.changes/v4.4.1.md
Normal file
3
.changes/v4.4.1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v4.4.1 - 2025-09-11
|
||||
### Fixed
|
||||
* fix translations in duplicate evaluation document modal and realign close modal button
|
3
.changes/v4.4.2.md
Normal file
3
.changes/v4.4.2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v4.4.2 - 2025-09-12
|
||||
### Fixed
|
||||
* Fix document generation and workflow generation do not work on accompanying period work documents
|
@@ -27,11 +27,11 @@ Chill is a comprehensive web application built as a set of Symfony bundles. It i
|
||||
|
||||
## 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:
|
||||
- `/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
|
||||
|
||||
Each bundle typically has the following structure:
|
||||
@@ -46,13 +46,13 @@ Each bundle typically has the following structure:
|
||||
|
||||
### 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
|
||||
|
||||
### 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`.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
5. **Set Up the Database**:
|
||||
6. **Set Up the Database**:
|
||||
```bash
|
||||
# Create the database
|
||||
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
|
||||
```
|
||||
|
||||
6. **Build Assets**:
|
||||
7. **Build Assets**:
|
||||
```bash
|
||||
nvm use 20
|
||||
yarn run encore dev
|
||||
```
|
||||
|
||||
7. **Start the Development Server**:
|
||||
8. **Start the Development Server**:
|
||||
```bash
|
||||
symfony server:start -d
|
||||
```
|
||||
|
||||
#### Docker Setup
|
||||
|
||||
The project includes Docker configuration for easier development:
|
||||
The project includes a Docker configuration for easier development:
|
||||
|
||||
1. **Start Docker Services**:
|
||||
```bash
|
||||
@@ -153,9 +153,9 @@ Key configuration files:
|
||||
|
||||
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\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
|
||||
`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`
|
||||
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).
|
||||
|
||||
##### 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`;
|
||||
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
|
||||
@@ -297,7 +297,7 @@ class TicketTest extends TestCase
|
||||
|
||||
#### 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
|
||||
|
||||
|
35
CHANGELOG.md
35
CHANGELOG.md
@@ -6,6 +6,41 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v4.4.2 - 2025-09-12
|
||||
### Fixed
|
||||
* Fix document generation and workflow generation do not work on accompanying period work documents
|
||||
|
||||
## v4.4.1 - 2025-09-11
|
||||
### Fixed
|
||||
* fix translations in duplicate evaluation document modal and realign close modal button
|
||||
|
||||
## v4.4.0 - 2025-09-11
|
||||
### Feature
|
||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
|
||||
* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation
|
||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works
|
||||
### Fixed
|
||||
* Fix display of 'duplicate' and 'merge' buttons in CRUD templates
|
||||
* Fix saving notification preferences in user's profile
|
||||
|
||||
## 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
|
||||
|
@@ -55,5 +55,6 @@
|
||||
</dl>
|
||||
|
||||
{% endblock %}
|
||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
||||
|
@@ -70,6 +70,8 @@
|
||||
<option value="00:10:00">10 minutes</option>
|
||||
<option value="00:15:00">15 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>
|
||||
<label class="input-group-text" for="slotMinTime">De</label>
|
||||
<select
|
||||
|
@@ -32,6 +32,8 @@
|
||||
<option value="00:10:00">10 minutes</option>
|
||||
<option value="00:15:00">15 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>
|
||||
<label class="input-group-text" for="slotMinTime">De</label>
|
||||
<select
|
||||
@@ -102,7 +104,8 @@
|
||||
event.title
|
||||
}}</b>
|
||||
<b v-else-if="event.extendedProps.is === 'range'"
|
||||
>{{ formatDate(event.startStr) }} -
|
||||
>{{ formatDate(event.startStr, "time") }} -
|
||||
{{ formatDate(event.endStr, "time") }}:
|
||||
{{ event.extendedProps.locationName }}</b
|
||||
>
|
||||
<b v-else-if="event.extendedProps.is === 'local'">{{
|
||||
@@ -294,9 +297,26 @@ const nextWeeks = computed((): Weeks[] =>
|
||||
}),
|
||||
);
|
||||
|
||||
const formatDate = (datetime: string) => {
|
||||
console.log(typeof datetime);
|
||||
return ISOToDate(datetime);
|
||||
const formatDate = (datetime: string, format: null | "time" = null) => {
|
||||
const date = 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>({
|
||||
|
@@ -18,6 +18,7 @@ use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
|
||||
@@ -147,16 +148,11 @@ class StoredObjectManager implements StoredObjectManagerInterface
|
||||
public function writeContent(string $filename, string $encryptedContent): void
|
||||
{
|
||||
$fullPath = $this->buildPath($filename);
|
||||
$dir = Path::getDirectory($fullPath);
|
||||
|
||||
if (!$this->filesystem->exists($dir)) {
|
||||
$this->filesystem->mkdir($dir);
|
||||
}
|
||||
|
||||
$result = file_put_contents($fullPath, $encryptedContent);
|
||||
|
||||
if (false === $result) {
|
||||
throw StoredObjectManagerException::unableToStoreDocumentOnDisk();
|
||||
try {
|
||||
$this->filesystem->dumpFile($fullPath, $encryptedContent);
|
||||
} catch (IOExceptionInterface $exception) {
|
||||
throw StoredObjectManagerException::unableToStoreDocumentOnDisk($exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { StoredObject, StoredObjectVersion } from "../../types";
|
||||
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
|
||||
import { computed, reactive } from "vue";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import { DOCUMENT_ADD, trans } from "translator";
|
||||
|
||||
interface DropFileConfig {
|
||||
allowRemove: boolean;
|
||||
@@ -75,11 +76,9 @@ function closeModal(): void {
|
||||
@click="openModal"
|
||||
class="btn btn-create"
|
||||
>
|
||||
Ajouter un document
|
||||
</button>
|
||||
<button v-else @click="openModal" class="btn btn-edit">
|
||||
Remplacer le document
|
||||
{{ trans(DOCUMENT_ADD) }}
|
||||
</button>
|
||||
<button v-else @click="openModal" class="btn btn-edit"></button>
|
||||
<modal
|
||||
v-if="state.showModal"
|
||||
:modal-dialog-class="modalClasses"
|
||||
|
@@ -23,6 +23,8 @@ See the document: Voir le document
|
||||
|
||||
document:
|
||||
Any title: Aucun titre
|
||||
replace: Remplacer
|
||||
Add: Ajouter un document
|
||||
|
||||
generic_doc:
|
||||
filter:
|
||||
|
@@ -118,7 +118,7 @@
|
||||
|
||||
{{ entity.notes|chill_print_or_message("Aucune note", 'blockquote') }}
|
||||
{% endblock crud_content_view_details %}
|
||||
|
||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||
{% block content_view_actions_back %}
|
||||
<li class="cancel">
|
||||
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_job_report_index', { 'person': entity.person.id }) }}">
|
||||
|
@@ -46,6 +46,7 @@
|
||||
</dd>
|
||||
</dl>
|
||||
{% endblock crud_content_view_details %}
|
||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||
|
||||
{% block content_view_actions_back %}
|
||||
<li class="cancel">
|
||||
|
@@ -206,6 +206,8 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||
|
||||
{% block content_view_actions_after %}
|
||||
<li>
|
||||
<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 %}
|
||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||
|
||||
{% block content_view_actions_back %}
|
||||
<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->setAbsenceStart(null);
|
||||
$user->setAbsenceEnd(null);
|
||||
$em = $this->managerRegistry->getManager();
|
||||
$em->flush();
|
||||
|
||||
|
@@ -345,7 +345,7 @@ class ExportController extends AbstractController
|
||||
* @param array $dataExport Raw data from export 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) {
|
||||
$formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], null);
|
||||
@@ -365,7 +365,7 @@ class ExportController extends AbstractController
|
||||
$formExport->submit($dataExport);
|
||||
$dataExport = $formExport->getData();
|
||||
|
||||
if (\count($dataFormatter) > 0) {
|
||||
if (is_array($dataFormatter) && \count($dataFormatter) > 0) {
|
||||
$formFormatter = $this->createCreateFormExport(
|
||||
$alias,
|
||||
'generate_formatter',
|
||||
@@ -381,7 +381,7 @@ class ExportController extends AbstractController
|
||||
'export' => $dataExport['export']['export'] ?? [],
|
||||
'filters' => $dataExport['export']['filters'] ?? [],
|
||||
'aggregators' => $dataExport['export']['aggregators'] ?? [],
|
||||
'pick_formatter' => $dataExport['export']['pick_formatter']['alias'],
|
||||
'pick_formatter' => ($dataExport['export']['pick_formatter'] ?? [])['alias'] ?? '',
|
||||
'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\Validator\Context\ExecutionContextInterface;
|
||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* User.
|
||||
@@ -45,6 +46,8 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
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.
|
||||
*/
|
||||
@@ -157,6 +160,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
return $this->absenceStart;
|
||||
}
|
||||
|
||||
public function getAbsenceEnd(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->absenceEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes.
|
||||
*
|
||||
@@ -336,7 +344,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function setAbsenceEnd(?\DateTimeImmutable $absenceEnd): void
|
||||
{
|
||||
$this->absenceEnd = $absenceEnd;
|
||||
}
|
||||
|
||||
public function setAttributeByDomain(string $domain, string $key, $value): self
|
||||
{
|
||||
$this->attributes[$domain][$key] = $value;
|
||||
@@ -633,46 +652,82 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getNotificationFlags(): array
|
||||
private function getNotificationFlagData(string $flag): array
|
||||
{
|
||||
return $this->notificationFlags;
|
||||
}
|
||||
|
||||
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;
|
||||
return $this->notificationFlags[$flag] ?? [self::NOTIF_FLAG_IMMEDIATE_EMAIL];
|
||||
}
|
||||
|
||||
public function isNotificationSendImmediately(string $type): bool
|
||||
{
|
||||
if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) {
|
||||
return true;
|
||||
return $this->isNotificationForElement($type, self::NOTIF_FLAG_IMMEDIATE_EMAIL);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->isNotificationForElement($type, self::NOTIF_FLAG_DAILY_DIGEST);
|
||||
}
|
||||
|
||||
public function getLocale(): string
|
||||
{
|
||||
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;
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
@@ -72,10 +72,14 @@ class ExportConfigNormalizer
|
||||
}
|
||||
$serialized['aggregators'] = $aggregatorsSerialized;
|
||||
|
||||
$serialized['pick_formatter'] = $formData['pick_formatter'];
|
||||
$formatter = $this->exportManager->getFormatter($formData['pick_formatter']);
|
||||
$serialized['formatter']['form'] = $formatter->normalizeFormData($formData['formatter']);
|
||||
$serialized['formatter']['version'] = $formatter->getNormalizationVersion();
|
||||
if ($export instanceof ExportInterface) {
|
||||
$serialized['pick_formatter'] = $formData['pick_formatter'];
|
||||
$formatter = $this->exportManager->getFormatter($formData['pick_formatter']);
|
||||
$serialized['formatter']['form'] = $formatter->normalizeFormData($formData['formatter']);
|
||||
$serialized['formatter']['version'] = $formatter->getNormalizationVersion();
|
||||
} elseif ($export instanceof DirectExportInterface) {
|
||||
$serialized['formatter'] = ['form' => [], 'version' => 0];
|
||||
}
|
||||
|
||||
return $serialized;
|
||||
}
|
||||
@@ -87,7 +91,12 @@ class ExportConfigNormalizer
|
||||
public function denormalizeConfig(string $exportAlias, array $serializedData, bool $replaceDisabledByDefaultData = false): array
|
||||
{
|
||||
$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 = [];
|
||||
foreach ($serializedData['filters'] as $alias => $filterData) {
|
||||
@@ -117,8 +126,8 @@ class ExportConfigNormalizer
|
||||
'export' => $export->denormalizeFormData($serializedData['export']['form'], $serializedData['export']['version']),
|
||||
'filters' => $filtersConfig,
|
||||
'aggregators' => $aggregatorsConfig,
|
||||
'pick_formatter' => $serializedData['pick_formatter'],
|
||||
'formatter' => $formater->denormalizeFormData($serializedData['formatter']['form'], $serializedData['formatter']['version']),
|
||||
'pick_formatter' => $serializedData['pick_formatter'] ?? '',
|
||||
'formatter' => $formatter?->denormalizeFormData($serializedData['formatter']['form'], $serializedData['formatter']['version']),
|
||||
'centers' => [
|
||||
'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)),
|
||||
|
@@ -23,9 +23,14 @@ class AbsenceType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('absenceStart', ChillDateType::class, [
|
||||
'required' => true,
|
||||
'required' => false,
|
||||
'input' => 'datetime_immutable',
|
||||
'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;
|
||||
|
||||
use Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper;
|
||||
use Chill\MainBundle\Notification\NotificationFlagManager;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -30,27 +28,24 @@ class NotificationFlagsType extends AbstractType
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders));
|
||||
|
||||
foreach ($this->notificationFlagProviders as $flagProvider) {
|
||||
$flag = $flagProvider->getFlag();
|
||||
$builder->add($flag, FormType::class, [
|
||||
$flagBuilder = $builder->create($flag, options: [
|
||||
'label' => $flagProvider->getLabel(),
|
||||
'required' => false,
|
||||
'compound' => true,
|
||||
]);
|
||||
|
||||
$builder->get($flag)
|
||||
$flagBuilder
|
||||
->add('immediate_email', CheckboxType::class, [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('daily_email', CheckboxType::class, [
|
||||
->add('daily_digest', CheckboxType::class, [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
;
|
||||
$builder->add($flagBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +53,7 @@ class NotificationFlagsType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => null,
|
||||
'compound' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -11,31 +11,29 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
|
||||
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
||||
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
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
|
||||
->add('phonenumber', ChillPhoneNumberType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('notificationFlags', NotificationFlagsType::class, [
|
||||
'label' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('notificationFlags', NotificationFlagsType::class)
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => \Chill\MainBundle\Entity\User::class,
|
||||
'data_class' => UpdateProfileCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -105,6 +105,11 @@ class UserType extends AbstractType
|
||||
'required' => false,
|
||||
'input' => 'datetime_immutable',
|
||||
'label' => 'absence.Absence start',
|
||||
])
|
||||
->add('absenceEnd', ChillDateType::class, [
|
||||
'required' => false,
|
||||
'input' => 'datetime_immutable',
|
||||
'label' => 'absence.Absence end',
|
||||
]);
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
|
@@ -37,8 +37,13 @@ export const ISOToDate = (str: string | null): Date | 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);
|
||||
};
|
||||
|
||||
@@ -69,20 +74,19 @@ export const ISOToDatetime = (str: string | null): Date | null => {
|
||||
*
|
||||
*/
|
||||
export const datetimeToISO = (date: Date): string => {
|
||||
let cal, time, offset;
|
||||
cal = [
|
||||
const cal = [
|
||||
date.getFullYear(),
|
||||
(date.getMonth() + 1).toString().padStart(2, "0"),
|
||||
date.getDate().toString().padStart(2, "0"),
|
||||
].join("-");
|
||||
|
||||
time = [
|
||||
const time = [
|
||||
date.getHours().toString().padStart(2, "0"),
|
||||
date.getMinutes().toString().padStart(2, "0"),
|
||||
date.getSeconds().toString().padStart(2, "0"),
|
||||
].join(":");
|
||||
|
||||
offset = [
|
||||
const offset = [
|
||||
date.getTimezoneOffset() <= 0 ? "+" : "-",
|
||||
Math.abs(Math.floor(date.getTimezoneOffset() / 60))
|
||||
.toString()
|
||||
|
@@ -84,6 +84,8 @@ const emits = defineEmits<{
|
||||
}
|
||||
.modal-header .close {
|
||||
border-top-right-radius: 0.3rem;
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
/*
|
||||
* The following styles are auto-applied to elements with
|
||||
|
@@ -44,17 +44,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock content_view_actions_duplicate_link %}
|
||||
{% block content_view_actions_merge %}
|
||||
<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_merge %}{% endblock %}
|
||||
{% block content_view_actions_edit_link %}
|
||||
{% if chill_crud_action_exists(crud_name, 'edit') %}
|
||||
{% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %}
|
||||
|
@@ -280,11 +280,17 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pick_linked_entities_widget %}
|
||||
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}" />
|
||||
<div data-input-uniqid="{{ form.vars['uniqid'] }}" data-module="pick-linked-entities" data-pick-entities-type="{{ form.vars['pick-entities-type'] }}"
|
||||
></div>
|
||||
|
||||
{% block pick_linked_entities_widget %}
|
||||
<input type="hidden" {{ block('widget_attributes') }}
|
||||
{% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %}
|
||||
data-input-uniqid="{{ form.vars['uniqid'] }}"/>
|
||||
<div
|
||||
data-input-uniqid="{{ form.vars['uniqid'] }}"
|
||||
data-module="pick-linked-entities"
|
||||
data-pick-entities-type="{{ form.vars['pick-entities-type'] }}"
|
||||
data-suggested="{{ form.vars['suggested']|json_encode|escape('html_attr') }}"
|
||||
></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pick_postal_code_widget %}
|
||||
|
@@ -8,36 +8,36 @@
|
||||
|
||||
<div class="col-md-10">
|
||||
<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 %}
|
||||
<div>
|
||||
<p>{{ 'absence.You are listed as absent, as of'|trans }} {{ user.absenceStart|format_date('long') }}</p>
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li>
|
||||
<a href="{{ path('chill_main_user_absence_unset') }}"
|
||||
class="btn btn-delete">{{ 'absence.Unset absence'|trans }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<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 %}
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li>
|
||||
<a class="btn btn-delete" title="Modifier" href="{{ path('chill_main_user_absence_unset') }}">{{ 'absence.Unset absence'|trans }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-save" type="submit">
|
||||
{{ 'Save'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
@@ -5,7 +5,7 @@
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="fa fa-flash"></i>
|
||||
<i class="bi bi-lightning-fill"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% for menu in menus %}
|
||||
|
@@ -64,7 +64,7 @@
|
||||
{{ form_widget(flag.immediate_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@@ -79,7 +79,7 @@
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
{% 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.
|
||||
*
|
||||
* @param string $role
|
||||
*
|
||||
* @return string the title of the role
|
||||
*/
|
||||
public function getRoleTitle($role)
|
||||
public function getRoleTitle(string $role): ?string
|
||||
{
|
||||
$this->initializeRolesTitlesCache();
|
||||
|
||||
@@ -73,7 +69,7 @@ class RoleProvider
|
||||
/**
|
||||
* initialize the array for caching role and titles.
|
||||
*/
|
||||
private function initializeRolesTitlesCache()
|
||||
private function initializeRolesTitlesCache(): void
|
||||
{
|
||||
// break if already initialized
|
||||
if (null !== $this->rolesTitlesCache) {
|
||||
|
@@ -39,6 +39,8 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
||||
'label' => '',
|
||||
'email' => '',
|
||||
'isAbsent' => false,
|
||||
'absenceStart' => null,
|
||||
'absenceEnd' => null,
|
||||
];
|
||||
|
||||
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']
|
||||
);
|
||||
|
||||
$absenceDatesContext = array_merge(
|
||||
$context,
|
||||
['docgen:expects' => \DateTimeImmutable::class, 'groups' => 'docgen:read']
|
||||
);
|
||||
|
||||
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)];
|
||||
}
|
||||
@@ -99,6 +106,8 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
||||
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
|
||||
'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext),
|
||||
'isAbsent' => $object->isAbsent(),
|
||||
'absenceStart' => $this->normalizer->normalize($object->getAbsenceStart(), $format, $absenceDatesContext),
|
||||
'absenceEnd' => $this->normalizer->normalize($object->getAbsenceEnd(), $format, $absenceDatesContext),
|
||||
];
|
||||
|
||||
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');
|
||||
|
||||
// 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');
|
||||
|
||||
// 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->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()
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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->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',
|
||||
'isAbsent' => false,
|
||||
'main_center' => ['context' => Center::class],
|
||||
'absenceStart' => ['context' => \DateTimeImmutable::class],
|
||||
'absenceEnd' => ['context' => \DateTimeImmutable::class],
|
||||
]];
|
||||
|
||||
yield [$userNoPhone, 'docgen', ['docgen:expects' => User::class],
|
||||
@@ -120,6 +122,8 @@ final class UserNormalizerTest extends TestCase
|
||||
'text_without_absent' => 'AnotherUser',
|
||||
'isAbsent' => false,
|
||||
'main_center' => ['context' => Center::class],
|
||||
'absenceStart' => ['context' => \DateTimeImmutable::class],
|
||||
'absenceEnd' => ['context' => \DateTimeImmutable::class],
|
||||
]];
|
||||
|
||||
yield [null, 'docgen', ['docgen:expects' => User::class], [
|
||||
@@ -138,6 +142,8 @@ final class UserNormalizerTest extends TestCase
|
||||
'text_without_absent' => '',
|
||||
'isAbsent' => false,
|
||||
'main_center' => ['context' => Center::class],
|
||||
'absenceStart' => null,
|
||||
'absenceEnd' => null,
|
||||
]];
|
||||
}
|
||||
}
|
||||
|
@@ -113,3 +113,5 @@ services:
|
||||
Chill\MainBundle\Service\EntityInfo\ViewEntityInfoManager:
|
||||
arguments:
|
||||
$vienEntityInfoProviders: !tagged_iterator chill_main.entity_info_provider
|
||||
|
||||
Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler: ~
|
||||
|
@@ -80,3 +80,7 @@ services:
|
||||
Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand:
|
||||
tags:
|
||||
- {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
|
||||
By date: Filtrer par date
|
||||
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
|
||||
A: A
|
||||
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
|
||||
Absence start: Absent à partir du
|
||||
Absence end: Jusqu'au
|
||||
Absent: 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.
|
||||
Is absent: Absent?
|
||||
|
||||
|
@@ -40,3 +40,7 @@ workflow:
|
||||
|
||||
rolling_date:
|
||||
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."
|
||||
|
||||
|
@@ -18,6 +18,7 @@ use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepos
|
||||
use Chill\PersonBundle\Service\AccompanyingPeriodWork\AccompanyingPeriodWorkMergeService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
@@ -32,7 +33,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
|
||||
* @ParamConverter("accompanyingPeriodWork", options={"id": "acpw_id"})
|
||||
*/
|
||||
#[Route(path: '{_locale}/person/accompanying-period/work/{id}/assign-duplicate', name: 'chill_person_accompanying_period_work_assign_duplicate')]
|
||||
public function assignDuplicate(AccompanyingPeriodWork $acpw, Request $request)
|
||||
public function assignDuplicate(AccompanyingPeriodWork $acpw, Request $request): Response
|
||||
{
|
||||
$accompanyingPeriod = $acpw->getAccompanyingPeriod();
|
||||
|
||||
@@ -78,8 +79,8 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
|
||||
* @ParamConverter("acpw1", options={"id": "acpw1_id"})
|
||||
* @ParamConverter("acpw2", options={"id": "acpw2_id"})
|
||||
*/
|
||||
#[Route(path: '/{_locale}/person/{acpw1_id}/duplicate/{acpw2_id}/confirm', name: 'chill_person_acpw_duplicate_confirm')]
|
||||
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request)
|
||||
#[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): Response
|
||||
{
|
||||
$accompanyingPeriod = $acpw1->getAccompanyingPeriod();
|
||||
|
||||
@@ -98,6 +99,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
|
||||
$session->getFlashBag()->add('success', new TranslatableMessage('acpw_duplicate.Successfully merged'));
|
||||
}
|
||||
|
||||
|
||||
return $this->redirectToRoute('chill_person_accompanying_period_work_show', ['id' => $acpw1->getId()]);
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
|
||||
use Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument\AccompanyingPeriodWorkEvaluationDocumentDuplicator;
|
||||
@@ -24,15 +25,16 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
|
||||
class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
|
||||
readonly class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AccompanyingPeriodWorkEvaluationDocumentDuplicator $duplicator,
|
||||
private readonly Security $security,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ChillUrlGeneratorInterface $urlGenerator,
|
||||
private AccompanyingPeriodWorkEvaluationDocumentDuplicator $duplicator,
|
||||
private Security $security,
|
||||
private SerializerInterface $serializer,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ChillUrlGeneratorInterface $urlGenerator,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{id}/duplicate', methods: ['POST'])]
|
||||
@@ -56,6 +58,32 @@ class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ParamConverter("document", options={"id": "document_id"})
|
||||
* @ParamConverter("evaluation", options={"id": "evaluation_id"})
|
||||
*/
|
||||
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/duplicate', methods: ['POST'])]
|
||||
public function duplicateToEvaluationApi(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): Response
|
||||
{
|
||||
$work = $evaluation->getAccompanyingPeriodWork();
|
||||
|
||||
if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work)) {
|
||||
throw new AccessDeniedHttpException('not allowed to edit this accompanying period work');
|
||||
}
|
||||
|
||||
$duplicatedDocument = $this->duplicator->duplicateToEvaluation($document, $evaluation);
|
||||
|
||||
$this->entityManager->persist($duplicatedDocument);
|
||||
$this->entityManager->persist($duplicatedDocument->getStoredObject());
|
||||
$this->entityManager->persist($evaluation);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($duplicatedDocument, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/{_locale}/person/accompanying-course-work-evaluation-document/{id}/duplicate', name: 'chill_person_accompanying_period_work_evaluation_document_duplicate', methods: ['POST'])]
|
||||
public function duplicate(AccompanyingPeriodWorkEvaluationDocument $document): Response
|
||||
{
|
||||
|
@@ -0,0 +1,58 @@
|
||||
<?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\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
|
||||
readonly class AccompanyingPeriodWorkEvaluationDocumentMoveController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private SerializerInterface $serializer,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @ParamConverter("document", options={"id": "document_id"})
|
||||
* @ParamConverter("evaluation", options={"id": "evaluation_id"})
|
||||
*/
|
||||
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/move', methods: ['POST'])]
|
||||
public function moveToEvaluationApi(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): Response
|
||||
{
|
||||
$work = $evaluation->getAccompanyingPeriodWork();
|
||||
|
||||
if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work)) {
|
||||
throw new AccessDeniedHttpException('not allowed to edit this accompanying period work');
|
||||
}
|
||||
|
||||
$document->setAccompanyingPeriodWorkEvaluation($evaluation);
|
||||
|
||||
$this->entityManager->persist($document);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
@@ -98,16 +98,6 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
|
||||
|
||||
public function setAccompanyingPeriodWorkEvaluation(?AccompanyingPeriodWorkEvaluation $accompanyingPeriodWorkEvaluation): AccompanyingPeriodWorkEvaluationDocument
|
||||
{
|
||||
// if an evaluation is already associated, we cannot change the association (removing the association,
|
||||
// by setting a null value, is allowed.
|
||||
if (
|
||||
$this->accompanyingPeriodWorkEvaluation instanceof AccompanyingPeriodWorkEvaluation
|
||||
&& $accompanyingPeriodWorkEvaluation instanceof AccompanyingPeriodWorkEvaluation
|
||||
) {
|
||||
if ($this->accompanyingPeriodWorkEvaluation !== $accompanyingPeriodWorkEvaluation) {
|
||||
throw new \RuntimeException('It is not allowed to change the evaluation for a document');
|
||||
}
|
||||
}
|
||||
$this->accompanyingPeriodWorkEvaluation = $accompanyingPeriodWorkEvaluation;
|
||||
|
||||
return $this;
|
||||
|
@@ -24,7 +24,7 @@ class FindAccompanyingPeriodWorkType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('acpw', PickLinkedAccompanyingPeriodWorkType::class, [
|
||||
'label' => 'Social action',
|
||||
'label' => 'Accompanying period work',
|
||||
'multiple' => false,
|
||||
'accompanyingPeriod' => $options['accompanyingPeriod'],
|
||||
])
|
||||
|
@@ -16,18 +16,26 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class PickLinkedAccompanyingPeriodWorkType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly NormalizerInterface $normalizer) {}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['multiple'] = $options['multiple'];
|
||||
$view->vars['types'] = ['acpw'];
|
||||
$view->vars['uniqid'] = uniqid('pick_acpw_dyn');
|
||||
$view->vars['suggested'] = [];
|
||||
$view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
|
||||
$view->vars['submit_on_adding_new_entity'] = false;
|
||||
$view->vars['pick-entities-type'] = 'acpw';
|
||||
$view->vars['attr']['data-accompanying-period-id'] = $options['accompanyingPeriod']->getId();
|
||||
|
||||
foreach ($options['suggested'] as $suggestion) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($suggestion, 'json', ['groups' => 'read']);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
@@ -38,6 +46,7 @@ class PickLinkedAccompanyingPeriodWorkType extends AbstractType
|
||||
->setDefault('multiple', false)
|
||||
->setAllowedTypes('multiple', ['bool'])
|
||||
->setDefault('compound', false)
|
||||
->setDefault('suggested', [])
|
||||
->setDefault('as_id', false)
|
||||
->setAllowedTypes('as_id', ['bool'])
|
||||
->setDefault('submit_on_adding_new_entity', false)
|
||||
|
@@ -41,7 +41,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
methods: {
|
||||
pickWork: function (payload: { work: AccompanyingPeriodWork }) {
|
||||
console.log("payload", payload);
|
||||
input.value = payload.work.id.toString();
|
||||
|
||||
input.value = payload.work.id?.toString() ?? "";
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@@ -84,7 +84,7 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
|
||||
}
|
||||
|
||||
export interface AccompanyingPeriodWork {
|
||||
id: number;
|
||||
id?: number;
|
||||
accompanyingPeriod?: AccompanyingPeriod;
|
||||
accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
|
||||
createdAt?: string;
|
||||
|
@@ -972,7 +972,7 @@ div#workEditor {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
i.fa {
|
||||
& > i.fa {
|
||||
padding: 0.25rem;
|
||||
color: $white;
|
||||
|
||||
|
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-4 col-form-label visually-hidden">{{
|
||||
trans(EVALUATION_PUBLIC_COMMENT)
|
||||
}}</label>
|
||||
<div class="col-sm-12">
|
||||
<ckeditor
|
||||
:editor="ClassicEditor"
|
||||
:config="classicEditorConfig"
|
||||
:placeholder="trans(EVALUATION_COMMENT_PLACEHOLDER)"
|
||||
:value="comment"
|
||||
@input="$emit('update:comment', $event)"
|
||||
tag-name="textarea"
|
||||
></ckeditor>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
|
||||
import { ClassicEditor } from "ckeditor5";
|
||||
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
|
||||
import {
|
||||
EVALUATION_PUBLIC_COMMENT,
|
||||
EVALUATION_COMMENT_PLACEHOLDER,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
defineProps(["comment"]);
|
||||
defineEmits(["update:comment"]);
|
||||
</script>
|
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="row mb-3">
|
||||
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
|
||||
{{ trans(EVALUATION_STARTDATE) }}
|
||||
</label>
|
||||
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
type="date"
|
||||
:value="startDate"
|
||||
@input="$emit('update:startDate', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
|
||||
{{ trans(EVALUATION_ENDDATE) }}
|
||||
</label>
|
||||
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
type="date"
|
||||
:value="endDate"
|
||||
@input="$emit('update:endDate', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
|
||||
{{ trans(EVALUATION_MAXDATE) }}
|
||||
</label>
|
||||
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
type="date"
|
||||
:value="maxDate"
|
||||
@input="$emit('update:maxDate', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
|
||||
{{ trans(EVALUATION_WARNING_INTERVAL) }}
|
||||
</label>
|
||||
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
type="number"
|
||||
:value="warningInterval"
|
||||
@input="$emit('update:warningInterval', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
EVALUATION_STARTDATE,
|
||||
EVALUATION_ENDDATE,
|
||||
EVALUATION_MAXDATE,
|
||||
EVALUATION_WARNING_INTERVAL,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
defineProps(["startDate", "endDate", "maxDate", "warningInterval"]);
|
||||
defineEmits([
|
||||
"update:startDate",
|
||||
"update:endDate",
|
||||
"update:maxDate",
|
||||
"update:warningInterval",
|
||||
]);
|
||||
</script>
|
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="row mb-3">
|
||||
<h6>{{ trans(EVALUATION_DOCUMENT_ADD) }} :</h6>
|
||||
<pick-template
|
||||
entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation"
|
||||
:id="evaluation.id"
|
||||
:templates="templates"
|
||||
:preventDefaultMoveToGenerate="true"
|
||||
@go-to-generate-document="submitBeforeGenerate"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<label class="col-form-label">{{
|
||||
trans(EVALUATION_GENERATE_A_DOCUMENT)
|
||||
}}</label>
|
||||
</template>
|
||||
</pick-template>
|
||||
<div>
|
||||
<label class="col-form-label">{{
|
||||
trans(EVALUATION_DOCUMENT_UPLOAD)
|
||||
}}</label>
|
||||
<ul class="record_actions document-upload">
|
||||
<li>
|
||||
<drop-file-modal
|
||||
:allow-remove="false"
|
||||
@add-document="emit('addDocument', $event)"
|
||||
></drop-file-modal>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import PickTemplate from "ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue";
|
||||
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
|
||||
import {
|
||||
EVALUATION_DOCUMENT_ADD,
|
||||
EVALUATION_DOCUMENT_UPLOAD,
|
||||
EVALUATION_GENERATE_A_DOCUMENT,
|
||||
trans,
|
||||
} from "translator";
|
||||
import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const props = defineProps(["evaluation", "templates"]);
|
||||
const emit = defineEmits(["addDocument"]);
|
||||
|
||||
async function submitBeforeGenerate({ template }) {
|
||||
const callback = (data) => {
|
||||
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(
|
||||
(e) => e.key === props.evaluation.key,
|
||||
).id;
|
||||
|
||||
window.location.assign(
|
||||
buildLink(
|
||||
template,
|
||||
evaluationId,
|
||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation",
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return store.dispatch("submit", callback).catch((e) => {
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
ul.document-upload {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,455 @@
|
||||
<template>
|
||||
<div class="row mb-3">
|
||||
<h5>{{ trans(EVALUATION_DOCUMENTS) }} :</h5>
|
||||
<div class="flex-table">
|
||||
<div
|
||||
class="item-bloc"
|
||||
v-for="(d, i) in documents"
|
||||
:key="d.id"
|
||||
:class="[
|
||||
parseInt(docAnchorId) === d.id ? 'bg-blink' : 'nothing',
|
||||
]"
|
||||
>
|
||||
<div :id="'document_' + d.id" class="item-row">
|
||||
<div class="input-group input-group-lg mb-3 row">
|
||||
<label class="col-sm-3 col-form-label"
|
||||
>Titre du document:</label
|
||||
>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
class="form-control document-title"
|
||||
type="text"
|
||||
:value="d.title"
|
||||
:id="d.id"
|
||||
:data-key="i"
|
||||
@input="$emit('inputDocumentTitle', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<div class="item-col item-meta">
|
||||
<p v-if="d.createdBy" class="createdBy">
|
||||
Créé par {{ d.createdBy.text }}<br />
|
||||
Le
|
||||
{{
|
||||
$d(ISOToDatetime(d.createdAt.datetime), "long")
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<div class="item-col">
|
||||
<ul class="record_actions">
|
||||
<li
|
||||
v-if="
|
||||
d.workflows_availables.length > 0 ||
|
||||
d.workflows.length > 0
|
||||
"
|
||||
>
|
||||
<list-workflow-modal
|
||||
:workflows="d.workflows"
|
||||
:allowCreate="true"
|
||||
relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument"
|
||||
:relatedEntityId="d.id"
|
||||
:workflowsAvailables="
|
||||
d.workflows_availables
|
||||
"
|
||||
:preventDefaultMoveToGenerate="true"
|
||||
:goToGenerateWorkflowPayload="{ doc: d }"
|
||||
@go-to-generate-workflow="
|
||||
goToGenerateWorkflowEvaluationDocument
|
||||
"
|
||||
></list-workflow-modal>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-if="AmIRefferer"
|
||||
class="btn btn-notify"
|
||||
@click="
|
||||
$emit(
|
||||
'goToGenerateNotification',
|
||||
d,
|
||||
false,
|
||||
)
|
||||
"
|
||||
></button>
|
||||
<template v-else>
|
||||
<button
|
||||
id="btnGroupNotifyButtons"
|
||||
type="button"
|
||||
class="btn btn-notify dropdown-toggle"
|
||||
:title="
|
||||
trans(EVALUATION_NOTIFICATION_SEND)
|
||||
"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu"
|
||||
aria-labelledby="btnGroupNotifyButtons"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
goToGenerateDocumentNotification(
|
||||
d,
|
||||
false,
|
||||
)
|
||||
"
|
||||
>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_NOTIFICATION_NOTIFY_REFERRER,
|
||||
)
|
||||
}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
goToGenerateDocumentNotification(
|
||||
d,
|
||||
false,
|
||||
)
|
||||
"
|
||||
>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_NOTIFICATION_NOTIFY_ANY,
|
||||
)
|
||||
}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</li>
|
||||
<li>
|
||||
<document-action-buttons-group
|
||||
:stored-object="d.storedObject"
|
||||
:filename="d.title"
|
||||
:can-edit="true"
|
||||
:execute-before-leave="
|
||||
submitBeforeLeaveToEditor
|
||||
"
|
||||
:davLink="
|
||||
d.storedObject._links?.dav_link.href
|
||||
"
|
||||
:davLinkExpiration="
|
||||
d.storedObject._links?.dav_link
|
||||
.expiration
|
||||
"
|
||||
@on-stored-object-status-change="
|
||||
$emit('statusDocumentChanged', $event)
|
||||
"
|
||||
></document-action-buttons-group>
|
||||
</li>
|
||||
<!--replace document-->
|
||||
<li
|
||||
v-if="
|
||||
Number.isInteger(d.id) &&
|
||||
d.storedObject._permissions.canEdit
|
||||
"
|
||||
>
|
||||
<drop-file-modal
|
||||
:existing-doc="d.storedObject"
|
||||
:allow-remove="false"
|
||||
@add-document="
|
||||
(arg) =>
|
||||
replaceDocument(
|
||||
d,
|
||||
arg.stored_object,
|
||||
arg.stored_object_version,
|
||||
)
|
||||
"
|
||||
></drop-file-modal>
|
||||
</li>
|
||||
<li v-if="Number.isInteger(d.id)">
|
||||
<div class="duplicate-dropdown">
|
||||
<button
|
||||
class="btn btn-outline-primary dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i class="bi bi-lightning-fill"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<!--delete-->
|
||||
<li v-if="d.workflows.length === 0">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
$emit('removeDocument', d)
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="fa fa-trash-o"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_DOCUMENT_DELETE,
|
||||
)
|
||||
}}
|
||||
</a>
|
||||
</li>
|
||||
<!--duplicate document-->
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
$emit(
|
||||
'duplicateDocument',
|
||||
d,
|
||||
)
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="fa fa-copy"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_DOCUMENT_DUPLICATE_HERE,
|
||||
)
|
||||
}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
prepareDocumentDuplicationToWork(
|
||||
d,
|
||||
)
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="fa fa-copy"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
|
||||
)
|
||||
}}</a
|
||||
>
|
||||
</li>
|
||||
<!--move document-->
|
||||
<li
|
||||
v-if="
|
||||
d.storedObject._permissions
|
||||
.canEdit
|
||||
"
|
||||
>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
prepareDocumentMoveToWork(d)
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="fa fa-arrows"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_DOCUMENT_MOVE,
|
||||
)
|
||||
}}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AccompanyingPeriodWorkSelectorModal
|
||||
v-if="showAccompanyingPeriodSelector"
|
||||
v-model:selectedAcpw="selectedAcpw"
|
||||
:accompanying-period-id="accompanyingPeriodId"
|
||||
:is-evaluation-selector="true"
|
||||
:ignore-accompanying-period-work-ids="[]"
|
||||
@close-modal="showAccompanyingPeriodSelector = false"
|
||||
@update:selectedEvaluation="selectedEvaluation = $event"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
|
||||
import ListWorkflowModal from "ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue";
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
|
||||
import {
|
||||
EVALUATION_NOTIFICATION_NOTIFY_REFERRER,
|
||||
EVALUATION_NOTIFICATION_NOTIFY_ANY,
|
||||
EVALUATION_NOTIFICATION_SEND,
|
||||
EVALUATION_DOCUMENTS,
|
||||
EVALUATION_DOCUMENT_MOVE,
|
||||
EVALUATION_DOCUMENT_DELETE,
|
||||
EVALUATION_DOCUMENT_DUPLICATE_HERE,
|
||||
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
|
||||
trans,
|
||||
} from "translator";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import AccompanyingPeriodWorkSelectorModal from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue";
|
||||
import { buildLinkCreate } from "ChillMainAssets/lib/entity-workflow/api";
|
||||
import { buildLinkCreate as buildLinkCreateNotification } from "ChillMainAssets/lib/entity-notification/api";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
const props = defineProps([
|
||||
"documents",
|
||||
"docAnchorId",
|
||||
"accompanyingPeriodId",
|
||||
"evaluation",
|
||||
]);
|
||||
const emit = defineEmits([
|
||||
"inputDocumentTitle",
|
||||
"removeDocument",
|
||||
"duplicateDocument",
|
||||
"statusDocumentChanged",
|
||||
"duplicateDocumentToWork",
|
||||
]);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const showAccompanyingPeriodSelector = ref(false);
|
||||
const selectedEvaluation = ref(null);
|
||||
const selectedDocumentToDuplicate = ref(null);
|
||||
const selectedDocumentToMove = ref(null);
|
||||
|
||||
const AmIRefferer = computed(() => {
|
||||
return !(
|
||||
store.state.work.accompanyingPeriod.user &&
|
||||
store.state.me &&
|
||||
store.state.work.accompanyingPeriod.user.id !== store.state.me.id
|
||||
);
|
||||
});
|
||||
|
||||
const prepareDocumentDuplicationToWork = (d) => {
|
||||
selectedDocumentToDuplicate.value = d;
|
||||
/** ensure selectedDocumentToMove is null */
|
||||
selectedDocumentToMove.value = null;
|
||||
|
||||
showAccompanyingPeriodSelector.value = true;
|
||||
};
|
||||
|
||||
const prepareDocumentMoveToWork = (d) => {
|
||||
selectedDocumentToMove.value = d;
|
||||
/** ensure selectedDocumentToDuplicate is null */
|
||||
selectedDocumentToDuplicate.value = null;
|
||||
|
||||
showAccompanyingPeriodSelector.value = true;
|
||||
};
|
||||
|
||||
watch(selectedEvaluation, (val) => {
|
||||
if (selectedDocumentToDuplicate.value) {
|
||||
emit("duplicateDocumentToEvaluation", {
|
||||
evaluation: val,
|
||||
document: selectedDocumentToDuplicate.value,
|
||||
});
|
||||
} else {
|
||||
emit("moveDocumentToEvaluation", {
|
||||
evaluationDest: val,
|
||||
document: selectedDocumentToMove.value,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function goToGenerateWorkflowEvaluationDocument({
|
||||
workflowName,
|
||||
payload,
|
||||
}) {
|
||||
const callback = (data) => {
|
||||
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
|
||||
(e) => e.key === props.evaluation.key,
|
||||
);
|
||||
let updatedDocument = evaluation.documents.find(
|
||||
(d) => d.key === payload.doc.key,
|
||||
);
|
||||
window.location.assign(
|
||||
buildLinkCreate(
|
||||
workflowName,
|
||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
|
||||
updatedDocument.id,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return store.dispatch("submit", callback).catch((e) => {
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a document in the store with a new document.
|
||||
*
|
||||
* @param {Object} oldDocument - The document to be replaced.
|
||||
* @param {StoredObject} storedObject - The stored object of the new document.
|
||||
* @param {StoredObjectVersion} storedObjectVersion - The new version of the document
|
||||
* @return {void}
|
||||
*/
|
||||
async function replaceDocument(oldDocument, storedObject, storedObjectVersion) {
|
||||
let document = {
|
||||
type: "accompanying_period_work_evaluation_document",
|
||||
storedObject: storedObject,
|
||||
title: oldDocument.title,
|
||||
};
|
||||
|
||||
return store.commit("replaceDocument", {
|
||||
key: props.evaluation.key,
|
||||
document,
|
||||
oldDocument: oldDocument,
|
||||
stored_object_version: storedObjectVersion,
|
||||
});
|
||||
}
|
||||
|
||||
async function goToGenerateDocumentNotification(document, tos) {
|
||||
const callback = (data) => {
|
||||
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
|
||||
(e) => e.key === props.evaluation.key,
|
||||
);
|
||||
let updatedDocument = evaluation.documents.find(
|
||||
(d) => d.key === document.key,
|
||||
);
|
||||
window.location.assign(
|
||||
buildLinkCreateNotification(
|
||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
|
||||
updatedDocument.id,
|
||||
tos === true
|
||||
? store.state.work.accompanyingPeriod.user?.id
|
||||
: null,
|
||||
window.location.pathname +
|
||||
window.location.search +
|
||||
window.location.hash,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return store.dispatch("submit", callback).catch((e) => {
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
async function submitBeforeLeaveToEditor() {
|
||||
console.log("submit beore edit 2");
|
||||
// empty callback
|
||||
const callback = () => null;
|
||||
return store.dispatch("submit", callback).catch((e) => {
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
</script>
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="row mb-3">
|
||||
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
|
||||
{{ trans(EVALUATION_TIME_SPENT) }}
|
||||
</label>
|
||||
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
|
||||
<select
|
||||
class="form-control form-control-sm"
|
||||
:value="timeSpent"
|
||||
@input="$emit('update:timeSpent', $event.target.value)"
|
||||
>
|
||||
<option disabled value="">
|
||||
{{ trans(EVALUATION_TIME_SPENT) }}
|
||||
</option>
|
||||
<option
|
||||
v-for="time in timeSpentChoices"
|
||||
:value="time.value"
|
||||
:key="time.value"
|
||||
>
|
||||
{{ time.text }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { EVALUATION_TIME_SPENT, trans } from "translator";
|
||||
|
||||
defineProps(["timeSpent", "timeSpentChoices"]);
|
||||
defineEmits(["update:timeSpent"]);
|
||||
</script>
|
@@ -11,12 +11,13 @@ import { findSocialActionsBySocialIssue } from "ChillPersonAssets/vuejs/_api/Soc
|
||||
import { create } from "ChillPersonAssets/vuejs/_api/AccompanyingCourseWork.js";
|
||||
import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods.ts";
|
||||
import { fetchTemplates } from "ChillDocGeneratorAssets/api/pickTemplate.js";
|
||||
import { duplicate } from "../_api/accompanyingCourseWorkEvaluationDocument";
|
||||
import {
|
||||
duplicate,
|
||||
duplicateDocumentToEvaluation,
|
||||
moveDocumentToEvaluation,
|
||||
} from "../_api/accompanyingCourseWorkEvaluationDocument";
|
||||
|
||||
const debug = process.env.NODE_ENV !== "production";
|
||||
const evalFQDN = encodeURIComponent(
|
||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation",
|
||||
);
|
||||
|
||||
const store = createStore({
|
||||
strict: debug,
|
||||
@@ -298,15 +299,47 @@ const store = createStore({
|
||||
);
|
||||
},
|
||||
addDuplicatedDocument(state, { document, evaluation_key }) {
|
||||
console.log("add duplicated document", document);
|
||||
console.log("add duplicated dcuemnt - evaluation key", evaluation_key);
|
||||
|
||||
let evaluation = state.evaluationsPicked.find(
|
||||
(e) => e.key === evaluation_key,
|
||||
);
|
||||
document.key = evaluation.documents.length + 1;
|
||||
evaluation.documents.splice(0, 0, document);
|
||||
},
|
||||
addDuplicatedDocumentToEvaluation(state, { document, evaluation }) {
|
||||
let evaluationDest = state.evaluationsPicked.find(
|
||||
(e) => e.id === evaluation.id,
|
||||
);
|
||||
if (evaluationDest) {
|
||||
console.log("add duplicated document to evaluation", evaluationDest);
|
||||
document.key = evaluationDest.documents.length + 1;
|
||||
evaluationDest.documents.splice(0, 0, document);
|
||||
}
|
||||
},
|
||||
moveDocumentToEvaluation(
|
||||
state,
|
||||
{ evaluationInitial, evaluationDest, document },
|
||||
) {
|
||||
let evaluationA = state.evaluationsPicked.find(
|
||||
(e) => e.id === evaluationInitial.id,
|
||||
);
|
||||
let evaluationB = state.evaluationsPicked.find(
|
||||
(e) => e.id === evaluationDest.id,
|
||||
);
|
||||
|
||||
if (evaluationB) {
|
||||
// add document to chosen evaluation if evaluation is part of the same social work
|
||||
document.key = evaluationB.documents.length + 1;
|
||||
evaluationB.documents.splice(0, 0, document);
|
||||
}
|
||||
|
||||
// remove document from original evaluation
|
||||
const indexToRemove = evaluationA.documents.findIndex(
|
||||
(doc) => doc.id === document.id,
|
||||
);
|
||||
if (indexToRemove !== -1) {
|
||||
evaluationA.documents.splice(indexToRemove, 1);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Replaces a document in the state with a new document.
|
||||
*
|
||||
@@ -603,6 +636,44 @@ const store = createStore({
|
||||
const newDoc = await duplicate(document.id);
|
||||
commit("addDuplicatedDocument", { document: newDoc, evaluation_key });
|
||||
},
|
||||
async duplicateDocumentToEvaluation({ commit }, { document, evaluation }) {
|
||||
try {
|
||||
const newDoc = await duplicateDocumentToEvaluation(
|
||||
document.id,
|
||||
evaluation.id,
|
||||
);
|
||||
commit("addDuplicatedDocumentToEvaluation", {
|
||||
document: newDoc,
|
||||
evaluation,
|
||||
});
|
||||
|
||||
return newDoc;
|
||||
} catch (error) {
|
||||
console.error("Failed to move document:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async moveDocumentToEvaluation(
|
||||
{ commit },
|
||||
{ evaluationInitial, evaluationDest, document },
|
||||
) {
|
||||
try {
|
||||
const response = await moveDocumentToEvaluation(
|
||||
document.id,
|
||||
evaluationDest.id,
|
||||
);
|
||||
commit("moveDocumentToEvaluation", {
|
||||
evaluationInitial,
|
||||
evaluationDest,
|
||||
document,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Failed to move document:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
removeDocument({ commit }, payload) {
|
||||
commit("removeDocument", payload);
|
||||
},
|
||||
|
@@ -9,3 +9,23 @@ export const duplicate = async (
|
||||
`/api/1.0/person/accompanying-course-work-evaluation-document/${id}/duplicate`,
|
||||
);
|
||||
};
|
||||
|
||||
export const duplicateDocumentToEvaluation = async (
|
||||
document_id: number,
|
||||
evaluation_id: number,
|
||||
): Promise<AccompanyingPeriodWorkEvaluationDocument> => {
|
||||
return makeFetch<null, AccompanyingPeriodWorkEvaluationDocument>(
|
||||
"POST",
|
||||
`/api/1.0/person/accompanying-course-work-evaluation-document/${document_id}/evaluation/${evaluation_id}/duplicate`,
|
||||
);
|
||||
};
|
||||
|
||||
export const moveDocumentToEvaluation = async (
|
||||
document_id: number,
|
||||
evaluation_id: number,
|
||||
): Promise<AccompanyingPeriodWorkEvaluationDocument> => {
|
||||
return makeFetch<null, AccompanyingPeriodWorkEvaluationDocument>(
|
||||
"POST",
|
||||
`/api/1.0/person/accompanying-course-work-evaluation-document/${document_id}/evaluation/${evaluation_id}/move`,
|
||||
);
|
||||
};
|
||||
|
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<h2 class="badge-title">
|
||||
<span class="title_label"></span>
|
||||
<span class="title_action">
|
||||
<span>
|
||||
{{ trans(EVALUATION) }}:
|
||||
<span class="badge bg-light text-dark">
|
||||
{{ eval?.evaluation?.title.fr }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<ul class="small_in_title columns mt-1">
|
||||
<li>
|
||||
<span class="item-key">
|
||||
{{
|
||||
trans(
|
||||
ACCOMPANYING_COURSE_WORK_START_DATE,
|
||||
)
|
||||
}}
|
||||
:
|
||||
</span>
|
||||
<b>{{ formatDate(eval.startDate) }}</b>
|
||||
</li>
|
||||
|
||||
<li v-if="eval.endDate">
|
||||
<span class="item-key">
|
||||
{{
|
||||
trans(ACCOMPANYING_COURSE_WORK_END_DATE)
|
||||
}}
|
||||
:
|
||||
</span>
|
||||
<b>{{ formatDate(eval.endDate) }}</b>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ACCOMPANYING_COURSE_WORK_END_DATE,
|
||||
ACCOMPANYING_COURSE_WORK_START_DATE,
|
||||
EVALUATION,
|
||||
trans,
|
||||
} from "translator";
|
||||
import { ISOToDate } from "ChillMainAssets/chill/js/date";
|
||||
import { DateTime } from "ChillMainAssets/types";
|
||||
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const props = defineProps<{ eval: AccompanyingPeriodWorkEvaluation }>();
|
||||
const formatDate = (dateObject: DateTime) => {
|
||||
if (dateObject) {
|
||||
const parsedDate = ISOToDate(dateObject.datetime);
|
||||
if (parsedDate) {
|
||||
return new Intl.DateTimeFormat("default", {
|
||||
dateStyle: "short",
|
||||
}).format(parsedDate);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="results">
|
||||
<div
|
||||
v-for="evaluation in evaluations"
|
||||
:key="evaluation.id"
|
||||
class="list-item"
|
||||
>
|
||||
<label class="acpw-item">
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
:value="evaluation"
|
||||
v-model="selectedEvaluation"
|
||||
name="item"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<accompanying-period-work-evaluation-item :eval="evaluation" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
|
||||
import { defineProps, ref, watch } from "vue";
|
||||
import AccompanyingPeriodWorkEvaluationItem from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkEvaluationItem.vue";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const props = defineProps<{
|
||||
evaluations: AccompanyingPeriodWorkEvaluation[];
|
||||
}>();
|
||||
const selectedEvaluation = ref<AccompanyingPeriodWorkEvaluation | null>(null);
|
||||
|
||||
// eslint-disable-next-line vue/valid-define-emits
|
||||
const emit = defineEmits();
|
||||
|
||||
watch(selectedEvaluation, (newValue) => {
|
||||
emit("update:selectedEvaluation", newValue);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.acpw-item {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
@@ -26,14 +26,24 @@ import AccompanyingPeriodWorkItem from "./AccompanyingPeriodWorkItem.vue";
|
||||
import { AccompanyingPeriodWork } from "../../../types";
|
||||
import { defineProps, ref, watch } from "vue";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const props = defineProps<{
|
||||
accompanyingPeriodWorks: AccompanyingPeriodWork[];
|
||||
selectedAcpw?: AccompanyingPeriodWork | null;
|
||||
}>();
|
||||
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
|
||||
const selectedAcpw = ref<AccompanyingPeriodWork | null>(
|
||||
props.selectedAcpw ?? null,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line vue/valid-define-emits
|
||||
const emit = defineEmits();
|
||||
const emit = defineEmits<{
|
||||
"update:selectedAcpw": [value: AccompanyingPeriodWork | null];
|
||||
}>();
|
||||
|
||||
watch(
|
||||
() => props.selectedAcpw,
|
||||
(val) => {
|
||||
selectedAcpw.value = val ?? null;
|
||||
},
|
||||
);
|
||||
|
||||
watch(selectedAcpw, (newValue) => {
|
||||
emit("update:selectedAcpw", newValue);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="row justify-content-end">
|
||||
<div class="row justify-content-end" v-if="!isEvaluationSelector">
|
||||
<div class="col-md-6 col-sm-10" v-if="selectedAcpw">
|
||||
<ul class="list-suggest remove-items">
|
||||
<li>
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="record_actions">
|
||||
<ul v-if="!showModal" class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-sm btn-create mt-3" @click="openModal">
|
||||
{{ trans(ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK) }}
|
||||
@@ -30,19 +30,21 @@
|
||||
>
|
||||
<template #header>
|
||||
<h3>
|
||||
{{
|
||||
trans(
|
||||
ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
|
||||
)
|
||||
}}
|
||||
{{ getModalTitle() }}
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<accompanying-period-work-list
|
||||
v-if="evaluations.length === 0"
|
||||
:accompanying-period-works="accompanyingPeriodWorks"
|
||||
v-model:selectedAcpw="selectedAcpw"
|
||||
/>
|
||||
<accompanying-period-work-evaluation-list
|
||||
v-if="evaluations.length > 0"
|
||||
:evaluations="evaluations"
|
||||
v-model:selectedEvaluation="selectedEvaluation"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
@@ -60,58 +62,116 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import AccompanyingPeriodWorkList from "./AccompanyingPeriodWorkList.vue";
|
||||
import { AccompanyingPeriodWork } from "../../../types";
|
||||
import {
|
||||
ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
|
||||
CONFIRM,
|
||||
trans,
|
||||
ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
|
||||
ACPW_DUPLICATE_SELECT_AN_EVALUATION,
|
||||
CONFIRM,
|
||||
} from "translator";
|
||||
import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
|
||||
|
||||
interface AccompanyingPeriodWorkSelectorModalProps {
|
||||
accompanyingPeriodId: number;
|
||||
}
|
||||
import AccompanyingPeriodWorkEvaluationList from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkEvaluationList.vue";
|
||||
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
|
||||
|
||||
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
|
||||
const selectedEvaluation = ref<AccompanyingPeriodWorkEvaluation | null>(null);
|
||||
const showModal = ref(false);
|
||||
const accompanyingPeriodWorks = ref<AccompanyingPeriodWork[]>([]);
|
||||
const props = defineProps<AccompanyingPeriodWorkSelectorModalProps>();
|
||||
const evaluations = ref<AccompanyingPeriodWorkEvaluation[]>([]);
|
||||
|
||||
const props = defineProps<{
|
||||
accompanyingPeriodId: string;
|
||||
isEvaluationSelector: boolean;
|
||||
ignoreAccompanyingPeriodWorkIds: number[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
pickWork: [payload: { work: AccompanyingPeriodWork | null }];
|
||||
closeModal: [];
|
||||
"update:selectedEvaluation": [evaluation: AccompanyingPeriodWorkEvaluation];
|
||||
}>();
|
||||
|
||||
const getModalTitle = () =>
|
||||
evaluations.value.length > 0
|
||||
? trans(ACPW_DUPLICATE_SELECT_AN_EVALUATION)
|
||||
: trans(ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.accompanyingPeriodId) {
|
||||
getAccompanyingPeriodWorks(props.accompanyingPeriodId);
|
||||
getAccompanyingPeriodWorks(parseInt(props.accompanyingPeriodId));
|
||||
} else {
|
||||
console.error("No accompanyingperiod id was given");
|
||||
}
|
||||
|
||||
showModal.value = true;
|
||||
});
|
||||
|
||||
const getAccompanyingPeriodWorks = async (periodId: number) => {
|
||||
const url = `/api/1.0/person/accompanying-course/${periodId}/works.json`;
|
||||
|
||||
try {
|
||||
accompanyingPeriodWorks.value = await fetchResults(url);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const accompanyingPeriodWorksFetched =
|
||||
await fetchResults<AccompanyingPeriodWork>(url);
|
||||
if (props.isEvaluationSelector) {
|
||||
accompanyingPeriodWorks.value = accompanyingPeriodWorksFetched.filter(
|
||||
(acpw: AccompanyingPeriodWork) =>
|
||||
acpw.accompanyingPeriodWorkEvaluations.length > 0 &&
|
||||
typeof acpw.id !== "undefined" &&
|
||||
!props.ignoreAccompanyingPeriodWorkIds.includes(acpw.id),
|
||||
);
|
||||
} else {
|
||||
accompanyingPeriodWorks.value = accompanyingPeriodWorksFetched;
|
||||
}
|
||||
/* makeFetch<number, AccompanyingPeriodWork[]>("GET", url)
|
||||
.then((response) => {
|
||||
accompanyingPeriodWorks.value = response;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});*/
|
||||
};
|
||||
|
||||
const openModal = () => (showModal.value = true);
|
||||
const closeModal = () => (showModal.value = false);
|
||||
watch(selectedAcpw, (newValue) => {
|
||||
const inputField = document.getElementById(
|
||||
"find_accompanying_period_work_acpw",
|
||||
) as HTMLInputElement;
|
||||
if (inputField) {
|
||||
inputField.value = String(newValue?.id || "");
|
||||
}
|
||||
|
||||
/* if (!props.isEvaluationSelector) {
|
||||
console.log("Emitting from watch:", { work: newValue });
|
||||
emit("pickWork", { work: newValue });
|
||||
}*/
|
||||
});
|
||||
|
||||
const openModal = () => {
|
||||
showModal.value = true;
|
||||
};
|
||||
const closeModal = () => {
|
||||
showModal.value = false;
|
||||
selectedEvaluation.value = null;
|
||||
// selectedAcpw.value = null;
|
||||
emit("closeModal");
|
||||
};
|
||||
const confirmSelection = () => {
|
||||
emit("pickWork", { work: selectedAcpw.value });
|
||||
closeModal();
|
||||
selectedAcpw.value = selectedAcpw.value;
|
||||
console.log("selectedAcpw", selectedAcpw.value);
|
||||
|
||||
if (!props.isEvaluationSelector) {
|
||||
if (selectedAcpw.value) {
|
||||
// only emit if something is actually selected!
|
||||
emit("pickWork", { work: selectedAcpw.value });
|
||||
closeModal();
|
||||
}
|
||||
// optionally show some error or warning if not selected
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAcpw.value && props.isEvaluationSelector) {
|
||||
evaluations.value =
|
||||
selectedAcpw.value.accompanyingPeriodWorkEvaluations;
|
||||
}
|
||||
|
||||
if (selectedEvaluation.value && props.isEvaluationSelector) {
|
||||
// console.log('evaluation log in modal', selectedEvaluation.value)
|
||||
emit("update:selectedEvaluation", selectedEvaluation.value);
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
{%- macro details(w, accompanyingCourse, options) -%}
|
||||
{% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with {
|
||||
'displayAction': false,
|
||||
'displayAction': true,
|
||||
'displayContent': 'short',
|
||||
'displayFontSmall': true,
|
||||
'itemBlocClass': '',
|
||||
'displayNotification': false
|
||||
'displayNotification': true
|
||||
} %}
|
||||
{% endmacro %}
|
||||
|
@@ -2,10 +2,10 @@
|
||||
|
||||
{% set activeRouteKey = 'chill_person_accompanying_period_work_assign_duplicate' %}
|
||||
|
||||
{% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %}
|
||||
|
||||
{% import '@ChillPerson/AccompanyingPeriodWorkDuplicate/_details.html.twig' as details %}
|
||||
|
||||
{% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="person-duplicate">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h3>
|
||||
<h1>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h1>
|
||||
{{ form_start(form) }}
|
||||
{%- if form.acpw is defined -%}
|
||||
{{ form_row(form.acpw) }}
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument;
|
||||
|
||||
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
@@ -36,4 +37,17 @@ class AccompanyingPeriodWorkEvaluationDocumentDuplicator
|
||||
|
||||
return $newDocument;
|
||||
}
|
||||
|
||||
public function duplicateToEvaluation(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): AccompanyingPeriodWorkEvaluationDocument
|
||||
{
|
||||
$newDocument = new AccompanyingPeriodWorkEvaluationDocument();
|
||||
$newDocument
|
||||
->setTitle($document->getTitle().' ('.$this->translator->trans('accompanying_course_evaluation_document.duplicated_at', ['at' => $this->clock->now()]).')')
|
||||
->setStoredObject($this->storedObjectDuplicate->duplicate($document->getStoredObject()))
|
||||
;
|
||||
|
||||
$evaluation->addDocument($newDocument);
|
||||
|
||||
return $newDocument;
|
||||
}
|
||||
}
|
||||
|
@@ -1993,3 +1993,33 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
|
||||
/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/duplicate:
|
||||
post:
|
||||
tags:
|
||||
- accompanying-course-work-evaluation-document
|
||||
summary: Dupliate an an accompanying period work evaluation document to another evaluation
|
||||
parameters:
|
||||
- in: path
|
||||
name: document_id
|
||||
required: true
|
||||
description: The document's id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
- in: path
|
||||
name: evaluation_id
|
||||
required: true
|
||||
description: The evaluation's id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
200:
|
||||
description: "OK"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
|
@@ -753,6 +753,42 @@ evaluation:
|
||||
delay: Délai
|
||||
notificationDelay: Délai de notification
|
||||
url: Lien internet
|
||||
title: Ecrire une évaluation
|
||||
status: Statut
|
||||
choose_a_status: Choisir un statut
|
||||
startdate: Date d'ouverture
|
||||
enddate: Date de fin
|
||||
maxdate: Date d'échéance
|
||||
warning_interval: Rappel (jours)
|
||||
public_comment: Note publique
|
||||
comment_placeholder: Commencez à écrire ...
|
||||
generate_a_document: Générer un document
|
||||
choose_a_template: Choisir un modèle
|
||||
add_a_document: Ajouter un document
|
||||
add: Ajouter une évaluation
|
||||
time_spent: Temps de rédaction
|
||||
select_time_spent: Indiquez le temps de rédaction
|
||||
Documents: Documents
|
||||
document_add: Générer ou téléverser un document
|
||||
document_upload: Téléverser un document
|
||||
document_title: Titre du document
|
||||
template_title: Nom du template
|
||||
browse: Ajouter un document
|
||||
replace: Remplacer
|
||||
download: Télécharger le fichier existant
|
||||
notification_notify_referrer: Notifier le référent
|
||||
notification_notify_any: Notifier d'autres utilisateurs
|
||||
notification_send: Envoyer une notification
|
||||
document:
|
||||
edit: Modifier
|
||||
delete: Supprimer
|
||||
move: Déplacer
|
||||
duplicate: Dupliquer
|
||||
duplicate_here: Dupliquer ici
|
||||
duplicate_to_other_evaluation: Dupliquer vers une autre évaluation
|
||||
duplicate_success: Le document d'évaluation a été dupliqué
|
||||
move_success: Le document d'évaluation a été déplacé
|
||||
|
||||
|
||||
goal:
|
||||
desactivationDate: Date de désactivation
|
||||
@@ -777,7 +813,6 @@ relation:
|
||||
reverseTitle: Deuxième membre
|
||||
|
||||
days: jours
|
||||
months: mois
|
||||
years: années
|
||||
|
||||
# specific to closing motive
|
||||
@@ -1508,7 +1543,8 @@ entity_display_title:
|
||||
acpw_duplicate:
|
||||
title: Fusionner les actions d'accompagnement
|
||||
description: Cette fusion conservera la date de début la plus ancienne, la date de fin la plus récente, toutes les évaluations, documents et workflows. Les agents traitants seront additionnés ainsi que les tiers intervenants. Les commentaires seront mis l'un à la suite de l'autre.
|
||||
Select accompanying period work: Selectionner un action d'accompagnement
|
||||
Select accompanying period work: Sélectionner une action d'accompagnement
|
||||
Select an evaluation: Sélectionner une évaluation
|
||||
Assign duplicate: Désigner un action d'accompagnement doublon
|
||||
Accompanying period work to delete: Action d'accompagnement à supprimer
|
||||
Accompanying period work to delete explanation: Cet action d'accompagnement sera supprimé.
|
||||
@@ -1525,3 +1561,6 @@ my_parcours_filters:
|
||||
parcours_intervening: Intervenant
|
||||
is_open: Parcours ouverts
|
||||
is_closed: Parcours clôturés
|
||||
|
||||
document_duplicate:
|
||||
to_evaluation_success: "Le document a été dupliquer"
|
||||
|
@@ -152,6 +152,17 @@
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% endblock %}
|
||||
{% block content_view_actions_merge %}
|
||||
<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_form_actions_delete %}{% endblock %}
|
||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||
{% endembed %}
|
||||
|
Reference in New Issue
Block a user