Compare commits

..

88 Commits

Author SHA1 Message Date
aabf62d399 Merge branch '369-duplicate-evaluation-document' into testing202509 2025-09-02 17:54:23 +02:00
d2d297a377 eslint fixes 2025-09-02 17:02:13 +02:00
0541995a60 Add icons to document action buttons and update bindings for accompanying period work IDs 2025-09-02 16:37:19 +02:00
29e054bd10 fix issues about icons to remain blank on blank
Refactor accompanying period work fetching logic to include a new filter for ignoring specific IDs, and update related components with new prop bindings.
2025-09-02 16:37:08 +02:00
da0099aafc Merge branch '369-duplicate-evaluation-document' into move-document-to-other-eval 2025-09-02 16:01:08 +02:00
3a18ea42fe Add ignore filter for accompanying period work IDs
Refactor accompanying period work fetching logic to include a new filter for ignoring specific IDs, and update related components with new prop bindings.
2025-09-02 15:52:06 +02:00
e60435b8cc Handle store state updates when moving documents
- Add null check to prevent error when evaluation is not part of this social work
2025-08-25 15:09:38 +02:00
ab6ab19499 feat: enhance document actions in UI
- Add edit and delete options in translation files
- Refactor `DocumentsList.vue` to group replace, delete, move, and duplicate actions in a dropdown menu
2025-08-25 15:02:26 +02:00
2a1762ea8d Remove code block in method setAccompanyingPeriodWorkEvaluation, no longer necessary 2025-08-25 15:02:26 +02:00
18ababbca9 Handle error when moving document between evaluations and display toast upon success 2025-08-25 15:02:26 +02:00
f6179cd3a3 WIP Add toast after successful move 2025-08-25 15:02:26 +02:00
ddf8da4cee php cs fixes 2025-08-25 15:02:26 +02:00
bf2181c2f1 allow changing evaluation for a document
- Remove restriction on changing evaluation in entity logic
2025-08-25 15:02:26 +02:00
d508fde8d2 enable moving documents between evaluations
- Add Vuex action and mutation for moving documents between evaluations
- Implement `moveDocumentToEvaluation` API method
- Update `DocumentsList.vue` and `FormEvaluation.vue` to handle move actions
2025-08-25 15:02:26 +02:00
14dba22181 wip: enable moving documents to evaluations
- Add `AccompanyingPeriodWorkEvaluationDocumentMoveController` for move functionality
- Update `DocumentsList.vue` to emit move event
- Adjust `FormEvaluation.vue` to handle move action
2025-08-25 15:02:26 +02:00
7dc7e77c62 WIP enable moving documents to evaluations
- Add new button and logic in `DocumentsList.vue` for moving documents
- Implement `moveDocumentToEvaluation` method in `FormEvaluation.vue`
- Ensure duplication and moving actions are mutually exclusive
- Add event for moving documents to evaluations
2025-08-25 15:02:26 +02:00
9d58904969 refactor: improve document duplication handling
- Remove unnecessary console logs
- Add null check before duplicating document to evaluation within another social work
2025-08-25 14:57:48 +02:00
4d90c7028f Store commit of document duplication only upon successful API call otherwise log error 2025-08-21 16:19:03 +02:00
3abb76d268 eslint 2025-08-21 09:55:52 +02:00
d62dd4396e Display toast upon successful duplication of evaluation document 2025-08-21 09:52:59 +02:00
59e8d9d516 Fix the access of results after API call 2025-08-20 16:03:56 +02:00
7dcb8abe38 Merge branch 'master' into 369-duplicate-evaluation-document 2025-08-20 15:28:19 +02:00
a0b2d92ba2 Fix the selection modal for acpw for merging functionality 2025-08-20 12:53:09 +02:00
7843e5dfd1 Add return types and remove unnecessary html snippet 2025-08-19 14:11:26 +02:00
32c847267b Remove dump 2025-08-19 10:20:38 +02:00
9b353f4d1b Filter accompanying period works in evaluation selector mode
- Add filtering to show only accompanying period works with evaluations in evaluation selector mode
2025-08-13 13:19:41 +02:00
81a858f07a eslint corrections 2025-08-13 12:38:32 +02:00
6a2ee232a9 feat: enable document duplication to another evaluation
- Introduce API method for duplicating a document to a different evaluation
- Add Vuex actions and mutations to handle duplication logic to another evaluation
2025-08-13 12:35:40 +02:00
56c43a0a76 Refactor display document duplication button and add translations
- Add new translations for document duplication and replacement options
- Adjust order of list elements in `DocumentsList.vue` for better readability
2025-08-13 09:40:36 +02:00
eb724a730c remove line ux-translator 2025-04-28 10:50:37 +02:00
18f98b6795 Changie added for fusion of accompanying period works 2025-04-03 10:09:16 +02:00
d73994edd0 Adjust display of acpw tag when modal in use for the selection of an evaluation 2025-04-03 10:05:43 +02:00
70603570c8 Changie added 2025-04-03 10:03:25 +02:00
df09dd2017 Eslint fixes 2025-04-03 10:02:17 +02:00
1c87280b1e Display a toast message when document is duplicated succesfully 2025-04-03 09:56:36 +02:00
445e093a28 Emit duplication of document to an evaluation and add backend logic 2025-04-02 19:03:58 +02:00
3f91c65b30 Display evaluations in modal after selection of accompanyingPeriodWork 2025-04-02 15:36:11 +02:00
9bc3c16b58 WIP prepare modal for display of evaluations linked to accompanying period work 2025-04-02 13:52:51 +02:00
12dff82248 Re-establish normal behavior for component within twig 2025-04-02 12:44:55 +02:00
ab23a4efb5 Refactor FormEvaluation.vue component 2025-04-02 11:55:37 +02:00
204fb20475 Change behavior of AccompanyingPeriodWorkSelectorModal.vue: open modal directly 2025-04-02 11:53:21 +02:00
f430d97152 Transform duplicate button into dropdown 2025-04-01 18:45:46 +02:00
4fa4d3b65c Phpstan and cs fixes 2025-03-27 14:32:06 +01:00
bd4c34cc1d Fix eslint issues and add ts interfaces for typing 2025-03-27 14:26:43 +01:00
4cea678e93 Fix updating of manyToMany relationships 2025-03-27 13:34:16 +01:00
5e6833975b Fix handling comments and workflows on acpw 2025-03-26 20:25:54 +01:00
f523b9adb3 Fix typing errors 2025-03-26 20:25:39 +01:00
a211549432 Adjust template and add translations 2025-03-26 15:16:27 +01:00
17b1363113 Fixes after rebase + apply item styling for accompanying course work 2025-03-26 14:08:45 +01:00
3356ed8e57 Correct for loop to display accompanying period list items 2025-03-24 16:13:56 +01:00
2a7fa517ee Only show merge button if there are more than 1 works attached to the parcours 2025-03-24 16:07:47 +01:00
85781c8e14 Use item renderbox for display of accompanyingperiodwork 2025-03-19 11:04:01 +01:00
00eb435896 Add chevron icon in merge button 2025-03-19 11:04:01 +01:00
ed71cffd6a Change behavior of information exchange between backend and frontend 2025-03-19 11:03:59 +01:00
ae679e6997 Fix merge service and passing of json to vue 2025-03-19 11:03:53 +01:00
e1d308fd97 WIP create new picker for accompanying period works 2025-03-19 11:03:42 +01:00
d9acda67e3 WIP dynamic picking of accompanying period work 2025-03-19 11:03:42 +01:00
e88da74882 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:03:41 +01:00
591c44d1a0 Create types 2025-03-19 11:03:18 +01:00
bf04b7981c Improve merge service according to specifications 2025-03-19 11:03:02 +01:00
df33eec30f WIP merge service 2025-03-19 11:03:00 +01:00
c657c98918 Styling and organization of components 2025-03-19 11:02:55 +01:00
ef5eb5b907 Open modal to select acpw 2025-03-19 11:02:27 +01:00
d683fe002d Different approach to creating acpw selector 2025-03-19 11:02:25 +01:00
555bbca59b WIP create new picker for accompanying period works 2025-03-19 11:02:22 +01:00
e9e9d5c458 WIP dynamic picking of accompanying period work 2025-03-19 11:02:22 +01:00
b1842a33ae WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:02:19 +01:00
6afeaccf24 Improve merge service according to specifications 2025-03-19 11:01:52 +01:00
fb76bac480 WIP merge service 2025-03-19 11:01:49 +01:00
6ded185289 Treat duplicate in backend and setup confirm page of merge 2025-03-19 11:00:40 +01:00
95adc29f9d WIP create new picker for accompanying period works 2025-03-19 11:00:40 +01:00
4d0c3e683f WIP dynamic picking of accompanying period work 2025-03-19 11:00:40 +01:00
018aafc773 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:00:40 +01:00
c4aea4efc2 Create types 2025-03-19 11:00:40 +01:00
225e3ca13f Improve merge service according to specifications 2025-03-19 11:00:40 +01:00
8c1fa7956a WIP merge service 2025-03-19 11:00:40 +01:00
e253d1b276 Styling and organization of components 2025-03-19 11:00:40 +01:00
a52aac2d98 Update package.json 2025-03-19 11:00:40 +01:00
9e8cf60dd8 Open modal to select acpw 2025-03-19 11:00:40 +01:00
7682d81d50 Different approach to creating acpw selector 2025-03-19 11:00:40 +01:00
5d31ce96c1 WIP create new picker for accompanying period works 2025-03-19 11:00:40 +01:00
81ef64a246 WIP dynamic picking of accompanying period work 2025-03-19 11:00:40 +01:00
49d1f78001 WIP fusion accompanyingperiodwork: controller, form, templates 2025-03-19 11:00:40 +01:00
0d0f3528e2 Add (temporary) types in Main and ThirdpartyBundle 2025-03-19 11:00:40 +01:00
d97d5e689a Create types 2025-03-19 11:00:40 +01:00
95d80ce13e Improve merge service according to specifications 2025-03-19 11:00:40 +01:00
668720984d WIP merge service 2025-03-19 11:00:40 +01:00
245c3fa121 First commit - changie for feature 2025-03-19 11:00:40 +01:00
60 changed files with 1498 additions and 1253 deletions

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Allow the merge of two accompanying period works
time: 2025-02-11T14:22:43.134106669+01:00
custom:
Issue: "359"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Duplication of a document to another accompanying period work evaluation
time: 2025-04-03T10:03:11.796736107+02:00
custom:
Issue: "369"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Fusion of two accompanying period works
time: 2025-04-03T10:08:57.25079018+02:00
custom:
Issue: "359"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
## 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

View File

@@ -1,10 +0,0 @@
## 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

View File

@@ -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 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.
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.
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 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).
- 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).
- `/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 fills a "Command," and a "CommandHandler" handles this command. They are saved in the `src/Bundle/ChillTicketBundle/src/Action` directory.
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.
## Development Guidelines
### Building and Configuration Instructions
All the commands should be run through the `symfony` command, which will configure the required variables.
All the command 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
```
6. **Set Up the Database**:
5. **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
```
7. **Build Assets**:
6. **Build Assets**:
```bash
nvm use 20
yarn run encore dev
```
8. **Start the Development Server**:
7. **Start the Development Server**:
```bash
symfony server:start -d
```
#### Docker Setup
The project includes a Docker configuration for easier development:
The project includes 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 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).
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).
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):
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):
- `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 a database, but usually possible in services.
where injection does not work when restoring an entity from 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 creating a mock
##### Useful helpers and tips that avoid create a mock
Some notable implementations that are test helpers and avoid creating a mock:
Some notable implementations that are tests helper, and avoid to create 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 a postgresql database filled with 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 postgresql database filled by fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file.
### Code Quality Tools

View File

@@ -6,24 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.3.0 - 2025-09-08
### Feature
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
* Add a command to generate a list of permissions
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
**Schema Change**: Add columns or tables
### Fixed
* fix date formatting in calendar range display
* Change route URL to avoid clash with person duplicate controller method
## v4.2.1 - 2025-09-03
### Fixed
* Fix exports to work with DirectExportInterface
### DX
* Improve error message when a stored object cannot be written on local disk
## v4.2.0 - 2025-09-02
### Feature
* ([#64](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/64)) Add external identifier for a Person

View File

@@ -70,8 +70,6 @@
<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

View File

@@ -32,8 +32,6 @@
<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
@@ -104,8 +102,7 @@
event.title
}}</b>
<b v-else-if="event.extendedProps.is === 'range'"
>{{ formatDate(event.startStr, "time") }} -
{{ formatDate(event.endStr, "time") }}:
>{{ formatDate(event.startStr) }} -
{{ event.extendedProps.locationName }}</b
>
<b v-else-if="event.extendedProps.is === 'local'">{{
@@ -297,26 +294,9 @@ const nextWeeks = computed((): Weeks[] =>
}),
);
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 formatDate = (datetime: string) => {
console.log(typeof datetime);
return ISOToDate(datetime);
};
const baseOptions = ref<CalendarOptions>({

View File

@@ -18,7 +18,6 @@ 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;
@@ -148,11 +147,16 @@ class StoredObjectManager implements StoredObjectManagerInterface
public function writeContent(string $filename, string $encryptedContent): void
{
$fullPath = $this->buildPath($filename);
$dir = Path::getDirectory($fullPath);
try {
$this->filesystem->dumpFile($fullPath, $encryptedContent);
} catch (IOExceptionInterface $exception) {
throw StoredObjectManagerException::unableToStoreDocumentOnDisk($exception);
if (!$this->filesystem->exists($dir)) {
$this->filesystem->mkdir($dir);
}
$result = file_put_contents($fullPath, $encryptedContent);
if (false === $result) {
throw StoredObjectManagerException::unableToStoreDocumentOnDisk();
}
}

View File

@@ -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_REPLACE, DOCUMENT_ADD, trans } from "translator";
interface DropFileConfig {
allowRemove: boolean;
@@ -75,10 +76,10 @@ function closeModal(): void {
@click="openModal"
class="btn btn-create"
>
Ajouter un document
{{ trans(DOCUMENT_ADD) }}
</button>
<button v-else @click="openModal" class="btn btn-edit">
Remplacer le document
<button v-else @click="openModal" class="dropdown-item">
{{ trans(DOCUMENT_REPLACE) }}
</button>
<modal
v-if="state.showModal"

View File

@@ -23,6 +23,8 @@ See the document: Voir le document
document:
Any title: Aucun titre
replace: Remplacer
Add: Ajouter un document
generic_doc:
filter:

View File

@@ -1,35 +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\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;
}
}

View File

@@ -48,7 +48,6 @@ class AbsenceController extends AbstractController
$user = $this->security->getUser();
$user->setAbsenceStart(null);
$user->setAbsenceEnd(null);
$em = $this->managerRegistry->getManager();
$em->flush();

View File

@@ -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 (is_array($dataFormatter) && \count($dataFormatter) > 0) {
if (\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'] ?? [],
];
}

View File

@@ -24,7 +24,6 @@ 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.
@@ -46,8 +45,6 @@ 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.
*/
@@ -160,11 +157,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->absenceStart;
}
public function getAbsenceEnd(): ?\DateTimeImmutable
{
return $this->absenceEnd;
}
/**
* Get attributes.
*
@@ -344,13 +336,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
public function isAbsent(): bool
{
$now = new \DateTimeImmutable('now');
$absenceStart = $this->getAbsenceStart();
$absenceEnd = $this->getAbsenceEnd();
return null !== $absenceStart
&& $absenceStart <= $now
&& (null === $absenceEnd || $now <= $absenceEnd);
return null !== $this->getAbsenceStart() && $this->getAbsenceStart() <= new \DateTimeImmutable('now');
}
/**
@@ -424,11 +410,6 @@ 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;
@@ -694,16 +675,4 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
{
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();
}
}
}

View File

@@ -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,14 +72,10 @@ class ExportConfigNormalizer
}
$serialized['aggregators'] = $aggregatorsSerialized;
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];
}
$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();
return $serialized;
}
@@ -91,12 +87,7 @@ class ExportConfigNormalizer
public function denormalizeConfig(string $exportAlias, array $serializedData, bool $replaceDisabledByDefaultData = false): array
{
$export = $this->exportManager->getExport($exportAlias);
if ($export instanceof ExportInterface) {
$formatter = $this->exportManager->getFormatter($serializedData['pick_formatter']);
} else {
$formatter = null;
}
$formater = $this->exportManager->getFormatter($serializedData['pick_formatter']);
$filtersConfig = [];
foreach ($serializedData['filters'] as $alias => $filterData) {
@@ -126,8 +117,8 @@ class ExportConfigNormalizer
'export' => $export->denormalizeFormData($serializedData['export']['form'], $serializedData['export']['version']),
'filters' => $filtersConfig,
'aggregators' => $aggregatorsConfig,
'pick_formatter' => $serializedData['pick_formatter'] ?? '',
'formatter' => $formatter?->denormalizeFormData($serializedData['formatter']['form'], $serializedData['formatter']['version']),
'pick_formatter' => $serializedData['pick_formatter'],
'formatter' => $formater->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)),

View File

@@ -23,14 +23,9 @@ class AbsenceType extends AbstractType
{
$builder
->add('absenceStart', ChillDateType::class, [
'required' => false,
'required' => true,
'input' => 'datetime_immutable',
'label' => 'absence.Absence start',
])
->add('absenceEnd', ChillDateType::class, [
'required' => false,
'input' => 'datetime_immutable',
'label' => 'absence.Absence end',
]);
}

View File

@@ -105,11 +105,6 @@ 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

View File

@@ -37,13 +37,8 @@ export const ISOToDate = (str: string | null): Date | null => {
return null;
}
// 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);
};
@@ -74,19 +69,20 @@ export const ISOToDatetime = (str: string | null): Date | null => {
*
*/
export const datetimeToISO = (date: Date): string => {
const cal = [
let cal, time, offset;
cal = [
date.getFullYear(),
(date.getMonth() + 1).toString().padStart(2, "0"),
date.getDate().toString().padStart(2, "0"),
].join("-");
const time = [
time = [
date.getHours().toString().padStart(2, "0"),
date.getMinutes().toString().padStart(2, "0"),
date.getSeconds().toString().padStart(2, "0"),
].join(":");
const offset = [
offset = [
date.getTimezoneOffset() <= 0 ? "+" : "-",
Math.abs(Math.floor(date.getTimezoneOffset() / 60))
.toString()

View File

@@ -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 %}

View File

@@ -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) }}
<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>
{% 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 %}
</div>
{% endblock %}

View File

@@ -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-delete" title="Modifier" href="{{ path('chill_main_user_absence_unset') }}">{{ 'absence.Unset absence'|trans }}</a>
<a class="btn btn-remove" title="Modifier" href="{{ path('chill_main_user_absence_index') }}">{{ 'absence.Unset absence'|trans }}</a>
</span>
</div>
{% endif %}

View File

@@ -1,86 +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\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
}
}

View File

@@ -52,8 +52,12 @@ class RoleProvider
/**
* Get the title for each role.
*
* @param string $role
*
* @return string the title of the role
*/
public function getRoleTitle(string $role): ?string
public function getRoleTitle($role)
{
$this->initializeRolesTitlesCache();
@@ -69,7 +73,7 @@ class RoleProvider
/**
* initialize the array for caching role and titles.
*/
private function initializeRolesTitlesCache(): void
private function initializeRolesTitlesCache()
{
// break if already initialized
if (null !== $this->rolesTitlesCache) {

View File

@@ -39,8 +39,6 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
'label' => '',
'email' => '',
'isAbsent' => false,
'absenceStart' => null,
'absenceEnd' => null,
];
public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) {}
@@ -79,11 +77,6 @@ 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)];
}
@@ -106,8 +99,6 @@ 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) {

View File

@@ -67,36 +67,4 @@ 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');
}
}

View File

@@ -1,98 +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\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);
}
}

View File

@@ -101,8 +101,6 @@ 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],
@@ -122,8 +120,6 @@ 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], [
@@ -142,8 +138,6 @@ final class UserNormalizerTest extends TestCase
'text_without_absent' => '',
'isAbsent' => false,
'main_center' => ['context' => Center::class],
'absenceStart' => null,
'absenceEnd' => null,
]];
}
}

View File

@@ -80,7 +80,3 @@ services:
Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand:
tags:
- {name: console.command}
Chill\MainBundle\Command\DumpListPermissionsCommand:
autoconfigure: true
autowire: true

View File

@@ -1,34 +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\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');
}
}

View File

@@ -136,7 +136,3 @@ 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}

View File

@@ -841,12 +841,12 @@ absence:
# single letter for absence
A: A
My absence: Mon absence
Unset absence: Supprimer mes dates d'absence
Unset absence: Supprimer la date 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?

View File

@@ -40,7 +40,3 @@ 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."

View File

@@ -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}/acpw-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}/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()]);
}

View File

@@ -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
{

View File

@@ -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
);
}
}

View File

@@ -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;

View File

@@ -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'],
])

View File

@@ -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)

View File

@@ -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() ?? "";
},
},
});

View File

@@ -84,7 +84,7 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
}
export interface AccompanyingPeriodWork {
id: number;
id?: number;
accompanyingPeriod?: AccompanyingPeriod;
accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
createdAt?: string;

View File

@@ -972,7 +972,7 @@ div#workEditor {
font-size: 85%;
}
i.fa {
& > i.fa {
padding: 0.25rem;
color: $white;

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,51 @@
<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="$emit('submitBeforeGenerate', $event)"
>
<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";
defineProps(["evaluation", "templates"]);
defineEmits(["addDocument", "submitBeforeGenerate"]);
</script>
<style scoped>
ul.document-upload {
justify-content: flex-start;
}
</style>

View File

@@ -0,0 +1,361 @@
<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="
$emit('goToGenerateWorkflow', $event)
"
></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"
>
&nbsp;
</button>
<ul
class="dropdown-menu"
aria-labelledby="btnGroupNotifyButtons"
>
<li>
<a
class="dropdown-item"
@click="
$emit(
'goToGenerateNotification',
d,
true,
)
"
>
{{
trans(
EVALUATION_NOTIFICATION_NOTIFY_REFERRER,
)
}}
</a>
</li>
<li>
<a
class="dropdown-item"
@click="
$emit(
'goToGenerateNotification',
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>
<li v-if="Number.isInteger(d.id)">
<div class="duplicate-dropdown">
<button
class="btn btn-edit dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{{ trans(EVALUATION_DOCUMENT_EDIT) }}
</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>
<!--replace document-->
<li
v-if="
d.storedObject._permissions
.canEdit
"
>
<drop-file-modal
:existing-doc="d.storedObject"
:allow-remove="false"
@add-document="
(arg) =>
$emit(
'replaceDocument',
d,
arg.stored_object,
arg.stored_object_version,
)
"
></drop-file-modal>
</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_EDIT,
EVALUATION_DOCUMENT_DUPLICATE_HERE,
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
trans,
} from "translator";
import { ref, watch } from "vue";
import AccompanyingPeriodWorkSelectorModal from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue";
defineProps([
"documents",
"docAnchorId",
"accompanyingPeriodId",
"accompanyingPeriodWorkId",
]);
const emit = defineEmits([
"inputDocumentTitle",
"removeDocument",
"duplicateDocument",
"statusDocumentChanged",
"goToGenerateWorkflow",
"goToGenerateNotification",
"duplicateDocumentToWork",
]);
const showAccompanyingPeriodSelector = ref(false);
const selectedEvaluation = ref(null);
const selectedDocumentToDuplicate = ref(null);
const selectedDocumentToMove = ref(null);
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,
});
}
});
</script>

View File

@@ -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>

View File

@@ -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);
},

View File

@@ -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`,
);
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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) }}
@@ -40,9 +40,15 @@
<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 +66,109 @@
</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 {
trans,
ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
CONFIRM,
trans,
} 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];
}>();
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>

View File

@@ -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 %}

View File

@@ -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) }}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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é dupliquer
move_success: Le document d'évaluation a été déplacer
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
@@ -1525,3 +1560,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"