mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-03 18:58:24 +00:00 
			
		
		
		
	Compare commits
	
		
			39 Commits
		
	
	
		
			v3.7.1
			...
			refactor_a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 724e6c7365 | |||
| a8f8c23027 | |||
| cb7e2d752e | |||
| 1019a7bcd4 | |||
| 9b0ae7198a | |||
| 30e7009178 | |||
| 
						
						
							
						
						ab35e8c034
	
				 | 
					
					
						|||
| 2aded2974f | |||
| 
						
						
							
						
						f84c1632b2
	
				 | 
					
					
						|||
| 99e4824137 | |||
| dacaaea235 | |||
| 
						
						
							
						
						096466e79e
	
				 | 
					
					
						|||
| 7285e5c2b0 | |||
| 37227a3aeb | |||
| 
						
						
							
						
						7569667189
	
				 | 
					
					
						|||
| b0993f4062 | |||
| 7c79b65f48 | |||
| b8f25bcd45 | |||
| f4efb0e975 | |||
| c641baec78 | |||
| cc150e32f0 | |||
| 26cf6459b4 | |||
| d0fa6dd512 | |||
| 03748a7e84 | |||
| 9e3431f397 | |||
| 912861dbff | |||
| 35f25daf7c | |||
| 21274155b5 | |||
| 
						
						
							
						
						3f7c136d6b
	
				 | 
					
					
						|||
| 5d9c573853 | |||
| 9a5fd67842 | |||
| 2755bc12c4 | |||
| 9e191f1b5b | |||
| 59fd9fc63f | |||
| eea1e40663 | |||
| 1b0771eb07 | |||
| 3a74c48104 | |||
| b37d7fb907 | |||
| 57b8dacba0 | 
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20250211-142243.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20250211-142243.yaml
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										11
									
								
								.changes/v3.8.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.changes/v3.8.0.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
## v3.8.0 - 2025-02-03
 | 
			
		||||
### Feature
 | 
			
		||||
* Improve the UX of the news item admin form to prevent wrong usage
 | 
			
		||||
* ([#319](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/319)) Notification list: display the concerned person's badges in the list
 | 
			
		||||
* ([#320](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/320)) Show the first 3 persons directly in the accompanying period's banner
 | 
			
		||||
* ([#334](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/334)) Suggest current user when creating an activity
 | 
			
		||||
* ([#331](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/331)) Add attachments to workflows
 | 
			
		||||
### Fixed
 | 
			
		||||
* ([#350](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/350)) Add validation error to manual selection of person in PersonDuplicateController
 | 
			
		||||
* ([#354](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/354)) Fix document category creation
 | 
			
		||||
* ([#351](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/351)) Add definitive whitespace between span elements in vue PersonText component
 | 
			
		||||
							
								
								
									
										3
									
								
								.changes/v3.8.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.8.1.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
## v3.8.1 - 2025-02-05
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix household link in the parcours banner   
 | 
			
		||||
							
								
								
									
										3
									
								
								.changes/v3.8.2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.8.2.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
## v3.8.2 - 2025-02-10
 | 
			
		||||
### Fixed
 | 
			
		||||
* ([#358](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/358)) Remove "filter" button on list of documents in the workflow's "add attachement" modal   
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -5,6 +5,7 @@ composer.lock
 | 
			
		||||
docs/build/
 | 
			
		||||
.php_cs.cache
 | 
			
		||||
.cache/*
 | 
			
		||||
yarn.lock
 | 
			
		||||
 | 
			
		||||
docker/db/data
 | 
			
		||||
docker/rabbitmq/data
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -6,6 +6,26 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
 | 
			
		||||
and is generated by [Changie](https://github.com/miniscruff/changie).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## v3.8.2 - 2025-02-10
 | 
			
		||||
### Fixed
 | 
			
		||||
* ([#358](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/358)) Remove "filter" button on list of documents in the workflow's "add attachement" modal   
 | 
			
		||||
 | 
			
		||||
## v3.8.1 - 2025-02-05
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix household link in the parcours banner   
 | 
			
		||||
 | 
			
		||||
## v3.8.0 - 2025-02-03
 | 
			
		||||
### Feature
 | 
			
		||||
* Improve the UX of the news item admin form to prevent wrong usage
 | 
			
		||||
* ([#319](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/319)) Notification list: display the concerned person's badges in the list
 | 
			
		||||
* ([#320](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/320)) Show the first 3 persons directly in the accompanying period's banner
 | 
			
		||||
* ([#334](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/334)) Suggest current user when creating an activity
 | 
			
		||||
* ([#331](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/331)) Add attachments to workflows
 | 
			
		||||
### Fixed
 | 
			
		||||
* ([#350](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/350)) Add validation error to manual selection of person in PersonDuplicateController
 | 
			
		||||
* ([#354](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/354)) Fix document category creation
 | 
			
		||||
* ([#351](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/351)) Add definitive whitespace between span elements in vue PersonText component
 | 
			
		||||
 | 
			
		||||
## v3.7.1 - 2025-01-21
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix legacy configuration processor for notifier component   
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								config/routes/chill_assets_dev.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								config/routes/chill_assets_dev.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
when@dev:
 | 
			
		||||
    sass_assets:
 | 
			
		||||
        path: /_dev/assets
 | 
			
		||||
        controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
 | 
			
		||||
        defaults:
 | 
			
		||||
            template: '@ChillMain/Dev/dev.assets.html.twig'
 | 
			
		||||
 | 
			
		||||
    sass_assets_test1:
 | 
			
		||||
        path: /_dev/assets_test1
 | 
			
		||||
        controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
 | 
			
		||||
        defaults:
 | 
			
		||||
            template: '@ChillMain/Dev/dev.assets.test1.html.twig'
 | 
			
		||||
 | 
			
		||||
    sass_assets_test2:
 | 
			
		||||
        path: /_dev/assets_test2
 | 
			
		||||
        controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
 | 
			
		||||
        defaults:
 | 
			
		||||
            template: '@ChillMain/Dev/dev.assets.test2.html.twig'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								config/routes/chill_swagger.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								config/routes/chill_swagger.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
when@dev:
 | 
			
		||||
    swagger_ui:
 | 
			
		||||
        path: /_dev/swagger
 | 
			
		||||
        controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
 | 
			
		||||
        defaults:
 | 
			
		||||
            template: '@ChillMain/Dev/swagger-ui/index.html.twig'
 | 
			
		||||
 | 
			
		||||
    swagger_specs:
 | 
			
		||||
        path: /_dev/specs.yaml
 | 
			
		||||
        controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
 | 
			
		||||
        defaults:
 | 
			
		||||
            template: api/specs.yaml
 | 
			
		||||
@@ -12,6 +12,8 @@ This runs eslint **not** taking the baseline into account, thus showing all exis
 | 
			
		||||
A script was also added to package.json allowing you to execute ``yarn run eslint``.
 | 
			
		||||
This will run eslint, but **taking the baseline into account**, thus only alerting to newly created errors.
 | 
			
		||||
 | 
			
		||||
The eslint command is configured to also run ``prettier`` which will simply format the code to look more uniform (takes care indentation for example).
 | 
			
		||||
 | 
			
		||||
Interesting options that can be used in combination with eslint are:
 | 
			
		||||
 | 
			
		||||
- ``--quiet`` to only get errors and silence the warnings
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							@@ -16,7 +16,7 @@
 | 
			
		||||
    "@eslint/js": "^9.14.0",
 | 
			
		||||
    "@luminateone/eslint-baseline": "^1.0.9",
 | 
			
		||||
    "@symfony/webpack-encore": "^4.1.0",
 | 
			
		||||
    "@tsconfig/node14": "^1.0.1",
 | 
			
		||||
    "@tsconfig/node20": "^20.1.4",
 | 
			
		||||
    "@types/dompurify": "^3.0.5",
 | 
			
		||||
    "@types/eslint__js": "^8.42.3",
 | 
			
		||||
    "@typescript-eslint/parser": "^8.12.2",
 | 
			
		||||
@@ -30,7 +30,6 @@
 | 
			
		||||
    "eslint-plugin-vue": "^9.30.0",
 | 
			
		||||
    "fork-awesome": "^1.1.7",
 | 
			
		||||
    "jquery": "^3.6.0",
 | 
			
		||||
    "marked": "^12.0.1",
 | 
			
		||||
    "node-sass": "^8.0.0",
 | 
			
		||||
    "popper.js": "^1.16.1",
 | 
			
		||||
    "postcss-loader": "^7.0.2",
 | 
			
		||||
@@ -80,7 +79,12 @@
 | 
			
		||||
    "dev": "encore dev",
 | 
			
		||||
    "watch": "encore dev --watch",
 | 
			
		||||
    "build": "encore production --progress",
 | 
			
		||||
    "eslint": "npx eslint-baseline --fix \"**/*.{js,ts,vue}\""
 | 
			
		||||
    "specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml> templates/api/specs.yaml",
 | 
			
		||||
    "specs-validate": "swagger-cli validate templates/api/specs.yaml",
 | 
			
		||||
    "specs-create-dir": "mkdir -p templates/api",
 | 
			
		||||
    "specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
 | 
			
		||||
    "version": "node --version",
 | 
			
		||||
    "eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
 | 
			
		||||
  },
 | 
			
		||||
  "private": true
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,10 +15,13 @@ use Chill\ActivityBundle\Entity\Activity;
 | 
			
		||||
use Chill\ActivityBundle\Repository\ActivityRepository;
 | 
			
		||||
use Chill\MainBundle\Entity\Notification;
 | 
			
		||||
use Chill\MainBundle\Notification\NotificationHandlerInterface;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Symfony\Component\Translation\TranslatableMessage;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatableInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class ActivityNotificationHandler implements NotificationHandlerInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private ActivityRepository $activityRepository) {}
 | 
			
		||||
    public function __construct(private ActivityRepository $activityRepository, private TranslatableStringHelperInterface $translatableStringHelper) {}
 | 
			
		||||
 | 
			
		||||
    public function getTemplate(Notification $notification, array $options = []): string
 | 
			
		||||
    {
 | 
			
		||||
@@ -37,4 +40,30 @@ final readonly class ActivityNotificationHandler implements NotificationHandlerI
 | 
			
		||||
    {
 | 
			
		||||
        return Activity::class === $notification->getRelatedEntityClass();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTitle(Notification $notification, array $options = []): TranslatableInterface
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $activity = $this->getRelatedEntity($notification)) {
 | 
			
		||||
            return new TranslatableMessage('activity.deleted');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new TranslatableMessage('activity.title', [
 | 
			
		||||
            'date' => $activity->getDate(),
 | 
			
		||||
            'type' => $this->translatableStringHelper->localize($activity->getActivityType()->getName()),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAssociatedPersons(Notification $notification, array $options = []): array
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $activity = $this->getRelatedEntity($notification)) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $activity->getPersonsAssociated();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getRelatedEntity(Notification $notification): ?Activity
 | 
			
		||||
    {
 | 
			
		||||
        return $this->activityRepository->find($notification->getRelatedEntityId());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import "es6-promise/auto";
 | 
			
		||||
import { createStore } from "vuex";
 | 
			
		||||
import { postLocation } from "./api";
 | 
			
		||||
import prepareLocations from "./store.locations.js";
 | 
			
		||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
 | 
			
		||||
 | 
			
		||||
const debug = process.env.NODE_ENV !== "production";
 | 
			
		||||
//console.log('window.activity', window.activity);
 | 
			
		||||
@@ -23,6 +24,7 @@ const removeIdFromValue = (string, id) => {
 | 
			
		||||
const store = createStore({
 | 
			
		||||
  strict: debug,
 | 
			
		||||
  state: {
 | 
			
		||||
    me: null,
 | 
			
		||||
    activity: window.activity,
 | 
			
		||||
    socialIssuesOther: [],
 | 
			
		||||
    socialActionsList: [],
 | 
			
		||||
@@ -79,15 +81,25 @@ const store = createStore({
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    suggestedUser(state) {
 | 
			
		||||
      // console.log('current user', state.me)
 | 
			
		||||
      const existingUserIds = state.activity.users.map((p) => p.id);
 | 
			
		||||
      return state.activity.activityType.usersVisible === 0
 | 
			
		||||
        ? []
 | 
			
		||||
        : [state.activity.accompanyingPeriod.user].filter(
 | 
			
		||||
            (u) => u !== null && !existingUserIds.includes(u.id),
 | 
			
		||||
          );
 | 
			
		||||
      let suggestedUsers =
 | 
			
		||||
        state.activity.activityType.usersVisible === 0
 | 
			
		||||
          ? []
 | 
			
		||||
          : [state.activity.accompanyingPeriod.user].filter(
 | 
			
		||||
              (u) => u !== null && !existingUserIds.includes(u.id),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
      // Add the current user from the state
 | 
			
		||||
      if (state.me && !existingUserIds.includes(state.me.id)) {
 | 
			
		||||
        suggestedUsers.push(state.me);
 | 
			
		||||
      }
 | 
			
		||||
      console.log("suggested users", suggestedUsers);
 | 
			
		||||
 | 
			
		||||
      return suggestedUsers;
 | 
			
		||||
    },
 | 
			
		||||
    suggestedResources(state) {
 | 
			
		||||
      const resources = state.activity.accompanyingPeriod.resources;
 | 
			
		||||
      // const resources = state.activity.accompanyingPeriod.resources;
 | 
			
		||||
      const existingPersonIds = state.activity.persons.map((p) => p.id);
 | 
			
		||||
      const existingThirdPartyIds = state.activity.thirdParties.map(
 | 
			
		||||
        (p) => p.id,
 | 
			
		||||
@@ -111,6 +123,9 @@ const store = createStore({
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  mutations: {
 | 
			
		||||
    setWhoAmI(state, me) {
 | 
			
		||||
      state.me = me;
 | 
			
		||||
    },
 | 
			
		||||
    // SocialIssueAcc
 | 
			
		||||
    addIssueInList(state, issue) {
 | 
			
		||||
      //console.log('add issue list', issue.id);
 | 
			
		||||
@@ -326,9 +341,17 @@ const store = createStore({
 | 
			
		||||
      }
 | 
			
		||||
      commit("updateLocation", value);
 | 
			
		||||
    },
 | 
			
		||||
    getWhoAmI({ commit }) {
 | 
			
		||||
      const url = `/api/1.0/main/whoami.json`;
 | 
			
		||||
      makeFetch("GET", url).then((user) => {
 | 
			
		||||
        commit("setWhoAmI", user);
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
store.dispatch("getWhoAmI");
 | 
			
		||||
 | 
			
		||||
prepareLocations(store);
 | 
			
		||||
 | 
			
		||||
export default store;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,83 +1,3 @@
 | 
			
		||||
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
 | 
			
		||||
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
 | 
			
		||||
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
 | 
			
		||||
 | 
			
		||||
{% set person_id = null %}
 | 
			
		||||
{% if activity.person %}
 | 
			
		||||
    {% set person_id = activity.person.id %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% set accompanying_course_id = null %}
 | 
			
		||||
{% if activity.accompanyingPeriod %}
 | 
			
		||||
    {% set accompanying_course_id = activity.accompanyingPeriod.id %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
<div class="item-bloc activity-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
 | 
			
		||||
    <div class="item-row">
 | 
			
		||||
        <div class="item-col" style="width: unset">
 | 
			
		||||
            {% if document.isPending %}
 | 
			
		||||
                <div class="badge text-bg-info" data-docgen-is-pending="{{ document.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
 | 
			
		||||
            {% elseif document.isFailure %}
 | 
			
		||||
                <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 | 
			
		||||
            <div>
 | 
			
		||||
                {% if activity.accompanyingPeriod is not null and context == 'person' %}
 | 
			
		||||
                    <span class="badge bg-primary">
 | 
			
		||||
                        <i class="fa fa-random"></i> {{ activity.accompanyingPeriod.id }}
 | 
			
		||||
                    </span> 
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                <div class="badge-activity-type">
 | 
			
		||||
                    <span class="title_label"></span>
 | 
			
		||||
                    <span class="title_action">
 | 
			
		||||
                    {{ activity.type.name | localize_translatable_string }}
 | 
			
		||||
                        {% if activity.emergency %}
 | 
			
		||||
                            <span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="denomination h2">
 | 
			
		||||
                {{ document.title|chill_print_or_message("No title") }}
 | 
			
		||||
            </div>
 | 
			
		||||
            {% if document.hasTemplate %}
 | 
			
		||||
                <div>
 | 
			
		||||
                    <p>{{ document.template.name|localize_translatable_string }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="item-col">
 | 
			
		||||
            <div class="container">
 | 
			
		||||
                <div class="dates row text-end">
 | 
			
		||||
                    <span>{{ document.createdAt|format_date('short') }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <div class="item-row separator">
 | 
			
		||||
        <div class="item-col item-meta">
 | 
			
		||||
            {{ mmm.createdBy(document) }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <ul class="item-col record_actions flex-shrink-1">
 | 
			
		||||
            {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    {{ document|chill_document_button_group(document.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: false})  }}
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if is_granted('CHILL_ACTIVITY_SEE', activity)%}
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-show"></a>
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="{{ chill_path_add_return_path('chill_activity_activity_edit', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-edit"></a>
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </ul>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
    {{ include('@ChillActivity/GenericDoc/activity_document_row.html.twig') }}
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,81 @@
 | 
			
		||||
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
 | 
			
		||||
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
 | 
			
		||||
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
 | 
			
		||||
 | 
			
		||||
{% set person_id = null %}
 | 
			
		||||
{% if activity.person %}
 | 
			
		||||
    {% set person_id = activity.person.id %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% set accompanying_course_id = null %}
 | 
			
		||||
{% if activity.accompanyingPeriod %}
 | 
			
		||||
    {% set accompanying_course_id = activity.accompanyingPeriod.id %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
<div class="item-row">
 | 
			
		||||
    <div class="item-col" style="width: unset">
 | 
			
		||||
        {% if document.isPending %}
 | 
			
		||||
            <div class="badge text-bg-info" data-docgen-is-pending="{{ document.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
 | 
			
		||||
        {% elseif document.isFailure %}
 | 
			
		||||
            <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
            {% if activity.accompanyingPeriod is not null and context == 'person' %}
 | 
			
		||||
                <span class="badge bg-primary">
 | 
			
		||||
                        <i class="fa fa-random"></i> {{ activity.accompanyingPeriod.id }}
 | 
			
		||||
                    </span> 
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            <div class="badge-activity-type">
 | 
			
		||||
                <span class="title_label"></span>
 | 
			
		||||
                <span class="title_action">
 | 
			
		||||
                    {{ activity.type.name | localize_translatable_string }}
 | 
			
		||||
                    {% if activity.emergency %}
 | 
			
		||||
                        <span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    </span>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="denomination h2">
 | 
			
		||||
            {{ document.title|chill_print_or_message("No title") }}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if document.hasTemplate %}
 | 
			
		||||
            <div>
 | 
			
		||||
                <p>{{ document.template.name|localize_translatable_string }}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="item-col">
 | 
			
		||||
        <div class="container">
 | 
			
		||||
            <div class="dates row text-end">
 | 
			
		||||
                <span>{{ document.createdAt|format_date('short') }}</span>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% if show_actions %}
 | 
			
		||||
    <div class="item-row separator">
 | 
			
		||||
        <div class="item-col item-meta">
 | 
			
		||||
            {{ mmm.createdBy(document) }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <ul class="item-col record_actions flex-shrink-1">
 | 
			
		||||
            {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    {{ document|chill_document_button_group(document.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: false})  }}
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if is_granted('CHILL_ACTIVITY_SEE', activity)%}
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-show"></a>
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="{{ chill_path_add_return_path('chill_activity_activity_edit', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-edit"></a>
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\ActivityBundle\Service\GenericDoc\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Chill\ActivityBundle\Service\GenericDoc\Providers\AccompanyingPeriodActivityGenericDocProvider;
 | 
			
		||||
use Chill\ActivityBundle\Service\GenericDoc\Renderers\AccompanyingPeriodActivityGenericDocRenderer;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
 | 
			
		||||
final readonly class AccompanyingPeriodActivityGenericDocNormalizer implements GenericDocNormalizerInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private StoredObjectRepositoryInterface $storedObjectRepository,
 | 
			
		||||
        private AccompanyingPeriodActivityGenericDocRenderer $renderer,
 | 
			
		||||
        private Environment $twig,
 | 
			
		||||
        private TranslatorInterface $translator,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
 | 
			
		||||
    {
 | 
			
		||||
        return AccompanyingPeriodActivityGenericDocProvider::KEY === $genericDocDTO->key
 | 
			
		||||
            && 'json' == $format;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
 | 
			
		||||
    {
 | 
			
		||||
        $storedObject = $this->storedObjectRepository->find($genericDocDTO->identifiers['id']);
 | 
			
		||||
 | 
			
		||||
        if (null === $storedObject) {
 | 
			
		||||
            return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'isPresent' => true,
 | 
			
		||||
            'title' => $storedObject->getTitle(),
 | 
			
		||||
            'html' => $this->twig->render(
 | 
			
		||||
                $this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
 | 
			
		||||
                $this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
 | 
			
		||||
            ),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,10 +13,12 @@ namespace Chill\ActivityBundle\Service\GenericDoc\Providers;
 | 
			
		||||
 | 
			
		||||
use Chill\ActivityBundle\Entity\Activity;
 | 
			
		||||
use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepositoryInterface;
 | 
			
		||||
use Chill\ActivityBundle\Repository\ActivityRepository;
 | 
			
		||||
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
@@ -34,8 +36,47 @@ final readonly class AccompanyingPeriodActivityGenericDocProvider implements Gen
 | 
			
		||||
        private EntityManagerInterface $em,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private ActivityDocumentACLAwareRepositoryInterface $activityDocumentACLAwareRepository,
 | 
			
		||||
        private ActivityRepository $activityRepository,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $activity = $this->getRelatedEntity($genericDocDTO->key, $genericDocDTO->identifiers)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $activity->getDocuments()->findFirst(fn (int $key, StoredObject $storedObject) => $storedObject->getId() === $genericDocDTO->identifiers['id']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
 | 
			
		||||
    {
 | 
			
		||||
        return self::KEY === $key && array_key_exists('activity_id', $identifiers);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getRelatedEntity(string $key, array $identifiers): ?Activity
 | 
			
		||||
    {
 | 
			
		||||
        return $this->activityRepository->find($identifiers['activity_id']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $activity = $this->getRelatedEntity($key, $identifiers)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new GenericDocDTO(
 | 
			
		||||
            self::KEY,
 | 
			
		||||
            $identifiers,
 | 
			
		||||
            \DateTimeImmutable::createFromInterface($activity->getDate()),
 | 
			
		||||
            $activity->getAccompanyingPeriod(),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
 | 
			
		||||
    {
 | 
			
		||||
        $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,9 @@ use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}>
 | 
			
		||||
 */
 | 
			
		||||
final readonly class AccompanyingPeriodActivityGenericDocRenderer implements GenericDocRendererInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private StoredObjectRepository $objectRepository, private ActivityRepository $activityRepository) {}
 | 
			
		||||
@@ -29,7 +32,8 @@ final readonly class AccompanyingPeriodActivityGenericDocRenderer implements Gen
 | 
			
		||||
 | 
			
		||||
    public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
 | 
			
		||||
    {
 | 
			
		||||
        return '@ChillActivity/GenericDoc/activity_document.html.twig';
 | 
			
		||||
        return ($options['row-only'] ?? false) ? '@ChillActivity/GenericDoc/activity_document_row.html.twig' :
 | 
			
		||||
            '@ChillActivity/GenericDoc/activity_document.html.twig';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
 | 
			
		||||
@@ -38,6 +42,7 @@ final readonly class AccompanyingPeriodActivityGenericDocRenderer implements Gen
 | 
			
		||||
            'activity' => $this->activityRepository->find($genericDocDTO->identifiers['activity_id']),
 | 
			
		||||
            'document' => $this->objectRepository->find($genericDocDTO->identifiers['id']),
 | 
			
		||||
            'context' => $genericDocDTO->getContext(),
 | 
			
		||||
            'show_actions' => $options['show-actions'] ?? true,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,3 +14,5 @@ export:
 | 
			
		||||
            describe_action_with_subject: >-
 | 
			
		||||
                Filtré par personne ayant eu un échange entre le {date_from, date} et le {date_to, date}, et un de ces sujets choisis: {reasons}
 | 
			
		||||
 | 
			
		||||
activity:
 | 
			
		||||
    title: Échange du {date, date, long} - {type}
 | 
			
		||||
 
 | 
			
		||||
@@ -101,6 +101,7 @@ activity:
 | 
			
		||||
    Insert a document: Insérer un document
 | 
			
		||||
    Remove a document: Supprimer le document
 | 
			
		||||
    comment: Commentaire
 | 
			
		||||
    deleted: Échange supprimé
 | 
			
		||||
No documents: Aucun document
 | 
			
		||||
 | 
			
		||||
# activity filter in list page
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\CalendarBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\CalendarBundle\Entity\CalendarDoc;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
@@ -49,4 +50,21 @@ class CalendarDocRepository implements ObjectRepository, CalendarDocRepositoryIn
 | 
			
		||||
    {
 | 
			
		||||
        return CalendarDoc::class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param StoredObject|int $storedObject the StoredObject instance, or the id of the stored object
 | 
			
		||||
     */
 | 
			
		||||
    public function findOneByStoredObject(StoredObject|int $storedObject): ?CalendarDoc
 | 
			
		||||
    {
 | 
			
		||||
        $storedObjectId = $storedObject instanceof StoredObject ? $storedObject->getId() : $storedObject;
 | 
			
		||||
 | 
			
		||||
        $qb = $this->repository->createQueryBuilder('c');
 | 
			
		||||
        $qb->where(
 | 
			
		||||
            $qb->expr()->eq(':storedObject', 'c.storedObject')
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $qb->setParameter('storedObject', $storedObjectId);
 | 
			
		||||
 | 
			
		||||
        return $qb->getQuery()->getOneOrNullResult();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\CalendarBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\CalendarBundle\Entity\CalendarDoc;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
 | 
			
		||||
interface CalendarDocRepositoryInterface
 | 
			
		||||
{
 | 
			
		||||
@@ -29,5 +30,7 @@ interface CalendarDocRepositoryInterface
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria): ?CalendarDoc;
 | 
			
		||||
 | 
			
		||||
    public function findOneByStoredObject(StoredObject|int $storedObject): ?CalendarDoc;
 | 
			
		||||
 | 
			
		||||
    public function getClassName();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -106,7 +106,10 @@ export default {
 | 
			
		||||
            });
 | 
			
		||||
            state.key = state.key + toAdd.length;
 | 
			
		||||
        },
 | 
			
		||||
        addExternals(state, externalEvents: (EventInput & { id: string })[]) {
 | 
			
		||||
        addExternals(
 | 
			
		||||
            state: CalendarRangesState,
 | 
			
		||||
            externalEvents: (EventInput & { id: string })[],
 | 
			
		||||
        ) {
 | 
			
		||||
            const toAdd = externalEvents.filter(
 | 
			
		||||
                (r) => !state.rangesIndex.has(r.id),
 | 
			
		||||
            );
 | 
			
		||||
@@ -160,7 +163,7 @@ export default {
 | 
			
		||||
                state.key = state.key + 1;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        updateRange(state, range: CalendarRange) {
 | 
			
		||||
        updateRange(state: CalendarRangesState, range: CalendarRange) {
 | 
			
		||||
            const found = state.ranges.find(
 | 
			
		||||
                (r) => r.calendarRangeId === range.id && r.is === "range",
 | 
			
		||||
            );
 | 
			
		||||
@@ -207,7 +210,7 @@ export default {
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        createRange(
 | 
			
		||||
            ctx,
 | 
			
		||||
            ctx: Context,
 | 
			
		||||
            {
 | 
			
		||||
                start,
 | 
			
		||||
                end,
 | 
			
		||||
@@ -253,10 +256,10 @@ export default {
 | 
			
		||||
                    throw error;
 | 
			
		||||
                });
 | 
			
		||||
        },
 | 
			
		||||
        deleteRange(ctx, calendarRangeId: number) {
 | 
			
		||||
        deleteRange(ctx: Context, calendarRangeId: number) {
 | 
			
		||||
            const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
 | 
			
		||||
 | 
			
		||||
            makeFetch<undefined, never>("DELETE", url).then((_) => {
 | 
			
		||||
            makeFetch<undefined, never>("DELETE", url).then(() => {
 | 
			
		||||
                ctx.commit("removeRange", calendarRangeId);
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
@@ -347,10 +350,10 @@ export default {
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Promise.all(promises).then((_) => Promise.resolve(null));
 | 
			
		||||
            return Promise.all(promises).then(() => Promise.resolve(null));
 | 
			
		||||
        },
 | 
			
		||||
        copyFromWeekToAnotherWeek(
 | 
			
		||||
            ctx,
 | 
			
		||||
            ctx: Context,
 | 
			
		||||
            { fromMonday, toMonday }: { fromMonday: Date; toMonday: Date },
 | 
			
		||||
        ): Promise<null> {
 | 
			
		||||
            const rangesToCopy: EventInputCalendarRange[] =
 | 
			
		||||
@@ -371,7 +374,7 @@ export default {
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Promise.all(promises).then((_) => Promise.resolve(null));
 | 
			
		||||
            return Promise.all(promises).then(() => Promise.resolve(null));
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
} as Module<CalendarRangesState, State>;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,71 +5,5 @@
 | 
			
		||||
{% set c = document.calendar %}
 | 
			
		||||
 | 
			
		||||
<div class="item-bloc">
 | 
			
		||||
    <div class="item-row">
 | 
			
		||||
        <div class="item-col" style="width: unset">
 | 
			
		||||
            {% if document.storedObject.isPending %}
 | 
			
		||||
                <div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
 | 
			
		||||
            {% elseif document.storedObject.isFailure %}
 | 
			
		||||
                <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 | 
			
		||||
            <div>
 | 
			
		||||
                {% if c.accompanyingPeriod is not null and context == 'person' %}
 | 
			
		||||
                    <span class="badge bg-primary">
 | 
			
		||||
                        <i class="fa fa-random"></i> {{ c.accompanyingPeriod.id }}
 | 
			
		||||
                    </span> 
 | 
			
		||||
                {% endif %}
 | 
			
		||||
 | 
			
		||||
                <span class="badge-calendar">
 | 
			
		||||
                    <span class="title_label"></span>
 | 
			
		||||
                    <span class="title_action">
 | 
			
		||||
                        {{ 'Calendar'|trans }}
 | 
			
		||||
                        {% if c.endDate.diff(c.startDate).days >= 1 %}
 | 
			
		||||
                            {{ c.startDate|format_datetime('short', 'short') }}
 | 
			
		||||
                            - {{ c.endDate|format_datetime('short', 'short') }}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            {{ c.startDate|format_datetime('short', 'short') }}
 | 
			
		||||
                            - {{ c.endDate|format_datetime('none', 'short') }}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </span>
 | 
			
		||||
                </span>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="denomination h2">
 | 
			
		||||
                {{ document.storedObject.title|chill_print_or_message("No title") }}
 | 
			
		||||
            </div>
 | 
			
		||||
            {% if document.storedObject.hasTemplate %}
 | 
			
		||||
                <div>
 | 
			
		||||
                    <p>{{ document.storedObject.template.name|localize_translatable_string }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="item-col">
 | 
			
		||||
            <div class="container">
 | 
			
		||||
                <div class="dates row text-end">
 | 
			
		||||
                    <span>{{ document.storedObject.createdAt|format_date('short') }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="item-row separator">
 | 
			
		||||
        <div class="item-col item-meta">
 | 
			
		||||
            {{ mmm.createdBy(document) }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <ul class="item-col record_actions flex-shrink-1">
 | 
			
		||||
            {% if is_granted('CHILL_CALENDAR_DOC_SEE', document) %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    {{ document.storedObject|chill_document_button_group(document.storedObject.title, is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c)) }}
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', {'id': c.id, 'docId': document.id}) }}" class="btn btn-edit"></a>
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </ul>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
    {{ include('@ChillCalendar/GenericDoc/calendar_document_row.html.twig') }}
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,75 @@
 | 
			
		||||
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
 | 
			
		||||
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
 | 
			
		||||
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
 | 
			
		||||
 | 
			
		||||
{% set c = document.calendar %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<div class="item-row">
 | 
			
		||||
    <div class="item-col" style="width: unset">
 | 
			
		||||
        {% if document.storedObject.isPending %}
 | 
			
		||||
            <div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
 | 
			
		||||
        {% elseif document.storedObject.isFailure %}
 | 
			
		||||
            <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
            {% if c.accompanyingPeriod is not null and context == 'person' %}
 | 
			
		||||
                <span class="badge bg-primary">
 | 
			
		||||
                        <i class="fa fa-random"></i> {{ c.accompanyingPeriod.id }}
 | 
			
		||||
                    </span> 
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 | 
			
		||||
            <span class="badge-calendar">
 | 
			
		||||
                    <span class="title_label"></span>
 | 
			
		||||
                    <span class="title_action">
 | 
			
		||||
                        {{ 'Calendar'|trans }}
 | 
			
		||||
                        {% if c.endDate.diff(c.startDate).days >= 1 %}
 | 
			
		||||
                            {{ c.startDate|format_datetime('short', 'short') }}
 | 
			
		||||
                            - {{ c.endDate|format_datetime('short', 'short') }}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            {{ c.startDate|format_datetime('short', 'short') }}
 | 
			
		||||
                            - {{ c.endDate|format_datetime('none', 'short') }}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </span>
 | 
			
		||||
                </span>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="denomination h2">
 | 
			
		||||
            {{ document.storedObject.title|chill_print_or_message("No title") }}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if document.storedObject.hasTemplate %}
 | 
			
		||||
            <div>
 | 
			
		||||
                <p>{{ document.storedObject.template.name|localize_translatable_string }}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="item-col">
 | 
			
		||||
        <div class="container">
 | 
			
		||||
            <div class="dates row text-end">
 | 
			
		||||
                <span>{{ document.storedObject.createdAt|format_date('short') }}</span>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% if show_actions %}
 | 
			
		||||
    <div class="item-row separator">
 | 
			
		||||
        <div class="item-col item-meta">
 | 
			
		||||
            {{ mmm.createdBy(document) }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <ul class="item-col record_actions flex-shrink-1">
 | 
			
		||||
            {% if is_granted('CHILL_CALENDAR_DOC_SEE', document) %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    {{ document.storedObject|chill_document_button_group(document.storedObject.title, is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c)) }}
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', {'id': c.id, 'docId': document.id}) }}" class="btn btn-edit"></a>
 | 
			
		||||
                </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\CalendarBundle\Service\GenericDoc\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Chill\CalendarBundle\Repository\CalendarDocRepositoryInterface;
 | 
			
		||||
use Chill\CalendarBundle\Service\GenericDoc\Providers\AccompanyingPeriodCalendarGenericDocProvider;
 | 
			
		||||
use Chill\CalendarBundle\Service\GenericDoc\Renderers\AccompanyingPeriodCalendarGenericDocRenderer;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
 | 
			
		||||
final readonly class AccompanyingPeriodCalendarGenericDocNormalizer implements GenericDocNormalizerInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private AccompanyingPeriodCalendarGenericDocRenderer $renderer,
 | 
			
		||||
        private CalendarDocRepositoryInterface $calendarDocRepository,
 | 
			
		||||
        private Environment $twig,
 | 
			
		||||
        private TranslatorInterface $translator,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
 | 
			
		||||
    {
 | 
			
		||||
        return AccompanyingPeriodCalendarGenericDocProvider::KEY === $genericDocDTO->key && 'json' === $format;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $calendarDoc = $this->calendarDocRepository->find($genericDocDTO->identifiers['id'])) {
 | 
			
		||||
            return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'isPresent' => true,
 | 
			
		||||
            'title' => $calendarDoc->getStoredObject()->getTitle(),
 | 
			
		||||
            'html' => $this->twig->render(
 | 
			
		||||
                $this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
 | 
			
		||||
                $this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true])
 | 
			
		||||
            ),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,10 +13,12 @@ namespace Chill\CalendarBundle\Service\GenericDoc\Providers;
 | 
			
		||||
 | 
			
		||||
use Chill\CalendarBundle\Entity\Calendar;
 | 
			
		||||
use Chill\CalendarBundle\Entity\CalendarDoc;
 | 
			
		||||
use Chill\CalendarBundle\Repository\CalendarDocRepositoryInterface;
 | 
			
		||||
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
@@ -38,8 +40,38 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private EntityManagerInterface $em,
 | 
			
		||||
        private CalendarDocRepositoryInterface $calendarRepository,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
 | 
			
		||||
    {
 | 
			
		||||
        return $this->calendarRepository->find($genericDocDTO->identifiers['id'])?->getStoredObject();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
 | 
			
		||||
    {
 | 
			
		||||
        return self::KEY === $key && array_key_exists('id', $identifiers);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $calendarDoc = $this->calendarRepository->find($identifiers['id'])) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new GenericDocDTO(
 | 
			
		||||
            self::KEY,
 | 
			
		||||
            $identifiers,
 | 
			
		||||
            \DateTimeImmutable::createFromInterface($calendarDoc->getCreatedAt() ?? new \DateTimeImmutable('now')),
 | 
			
		||||
            $calendarDoc->getCalendar()->getAccompanyingPeriod() ?? $calendarDoc->getCalendar()->getPerson()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws MappingException
 | 
			
		||||
     */
 | 
			
		||||
@@ -82,7 +114,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen
 | 
			
		||||
            [Types::INTEGER]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return $query;
 | 
			
		||||
        return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,9 @@ use Chill\CalendarBundle\Service\GenericDoc\Providers\PersonCalendarGenericDocPr
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}>
 | 
			
		||||
 */
 | 
			
		||||
final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements GenericDocRendererInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private CalendarDocRepository $repository) {}
 | 
			
		||||
@@ -28,7 +31,8 @@ final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements Gen
 | 
			
		||||
 | 
			
		||||
    public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
 | 
			
		||||
    {
 | 
			
		||||
        return '@ChillCalendar/GenericDoc/calendar_document.html.twig';
 | 
			
		||||
        return $options['row-only'] ?? false ? '@ChillCalendar/GenericDoc/calendar_document_row.html.twig'
 | 
			
		||||
            : '@ChillCalendar/GenericDoc/calendar_document.html.twig';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
 | 
			
		||||
@@ -36,6 +40,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements Gen
 | 
			
		||||
        return [
 | 
			
		||||
            'document' => $this->repository->find($genericDocDTO->identifiers['id']),
 | 
			
		||||
            'context' => $genericDocDTO->getContext(),
 | 
			
		||||
            'show_actions' => $options['show-actions'] ?? true,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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\DocGeneratorBundle\Tests\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Controller\GenericDocForAccompanyingPeriodListApiController;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
 | 
			
		||||
use Chill\MainBundle\Pagination\Paginator;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
 | 
			
		||||
use Chill\MainBundle\Serializer\Model\Collection;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\SerializerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class GenericDocForAccompanyingPeriodListApiControllerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    public function testSmokeTest(): void
 | 
			
		||||
    {
 | 
			
		||||
        $accompanyingPeriod = new AccompanyingPeriod();
 | 
			
		||||
 | 
			
		||||
        $docs = [
 | 
			
		||||
            new GenericDocDTO('dummy', ['id' => 9], new \DateTimeImmutable('2024-08-01'), $accompanyingPeriod),
 | 
			
		||||
            new GenericDocDTO('dummy', ['id' => 1], new \DateTimeImmutable('2024-09-01'), $accompanyingPeriod),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        $manager = $this->createMock(ManagerInterface::class);
 | 
			
		||||
        $manager->method('findDocForAccompanyingPeriod')->with($accompanyingPeriod)->willReturn($docs);
 | 
			
		||||
        $manager->method('countDocForAccompanyingPeriod')->with($accompanyingPeriod)->willReturn(2);
 | 
			
		||||
 | 
			
		||||
        $paginatorFactory = $this->createMock(PaginatorFactoryInterface::class);
 | 
			
		||||
        $paginatorFactory->method('create')->with(2)->willReturn(new Paginator(
 | 
			
		||||
            2,
 | 
			
		||||
            20,
 | 
			
		||||
            1,
 | 
			
		||||
            '/route',
 | 
			
		||||
            [],
 | 
			
		||||
            $this->createMock(UrlGeneratorInterface::class),
 | 
			
		||||
            'page',
 | 
			
		||||
            'item-per-page'
 | 
			
		||||
        ));
 | 
			
		||||
 | 
			
		||||
        $serializer = $this->createMock(SerializerInterface::class);
 | 
			
		||||
        $serializer->method('serialize')->with($this->isInstanceOf(Collection::class))->willReturn(
 | 
			
		||||
            json_encode(['docs' => []])
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $security = $this->createMock(Security::class);
 | 
			
		||||
        $security->expects($this->once())->method('isGranted')
 | 
			
		||||
            ->with(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)->willReturn(true);
 | 
			
		||||
 | 
			
		||||
        $controller = new GenericDocForAccompanyingPeriodListApiController(
 | 
			
		||||
            $manager,
 | 
			
		||||
            $security,
 | 
			
		||||
            $paginatorFactory,
 | 
			
		||||
            $serializer,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $response = $controller($accompanyingPeriod);
 | 
			
		||||
 | 
			
		||||
        $this->assertInstanceOf(JsonResponse::class, $response);
 | 
			
		||||
        $this->assertEquals('{"docs":[]}', $response->getContent());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle;
 | 
			
		||||
use Chill\DocStoreBundle\DependencyInjection\Compiler\StorageConfigurationCompilerPass;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
 | 
			
		||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
 | 
			
		||||
@@ -28,6 +29,8 @@ class ChillDocStoreBundle extends Bundle
 | 
			
		||||
            ->addTag('chill_doc_store.generic_doc_person_provider');
 | 
			
		||||
        $container->registerForAutoconfiguration(GenericDocRendererInterface::class)
 | 
			
		||||
            ->addTag('chill_doc_store.generic_doc_renderer');
 | 
			
		||||
        $container->registerForAutoconfiguration(GenericDocNormalizerInterface::class)
 | 
			
		||||
            ->addTag('chill_doc_store.generic_doc_metadata_normalizer');
 | 
			
		||||
 | 
			
		||||
        $container->addCompilerPass(new StorageConfigurationCompilerPass());
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -92,13 +92,14 @@ class DocumentCategoryController extends AbstractController
 | 
			
		||||
 | 
			
		||||
        $nextId = $em
 | 
			
		||||
            ->createQuery(
 | 
			
		||||
                'SELECT MAX(c.idInsideBundle) + 1 FROM ChillDocStoreBundle:DocumentCategory c'
 | 
			
		||||
                'SELECT (CASE WHEN MAX(c.idInsideBundle) IS NULL THEN 1 ELSE MAX(c.idInsideBundle) + 1 END)
 | 
			
		||||
         FROM ChillDocStoreBundle:DocumentCategory c'
 | 
			
		||||
            )
 | 
			
		||||
            ->getSingleResult();
 | 
			
		||||
            ->getSingleScalarResult();
 | 
			
		||||
 | 
			
		||||
        $documentCategory = new DocumentCategory(
 | 
			
		||||
            ChillDocStoreBundle::class,
 | 
			
		||||
            reset($nextId)
 | 
			
		||||
            $nextId
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $documentCategory
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Manager;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
 | 
			
		||||
@@ -25,7 +25,7 @@ final readonly class GenericDocForAccompanyingPeriodController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private FilterOrderHelperFactory $filterOrderHelperFactory,
 | 
			
		||||
        private Manager $manager,
 | 
			
		||||
        private ManagerInterface $manager,
 | 
			
		||||
        private PaginatorFactory $paginator,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private \Twig\Environment $twig,
 | 
			
		||||
@@ -68,6 +68,9 @@ final readonly class GenericDocForAccompanyingPeriodController
 | 
			
		||||
        );
 | 
			
		||||
        $paginator = $this->paginator->create($nb);
 | 
			
		||||
 | 
			
		||||
        // restrict the number of items for performance reasons
 | 
			
		||||
        $paginator->setItemsPerPage(20);
 | 
			
		||||
 | 
			
		||||
        $documents = $this->manager->findDocForAccompanyingPeriod(
 | 
			
		||||
            $accompanyingPeriod,
 | 
			
		||||
            $paginator->getCurrentPageFirstItemNumber(),
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
 | 
			
		||||
use Chill\MainBundle\Serializer\Model\Collection;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provide the list of GenericDoc for an accompanying period.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class GenericDocForAccompanyingPeriodListApiController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private ManagerInterface $manager,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private PaginatorFactoryInterface $paginator,
 | 
			
		||||
        private SerializerInterface $serializer,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/doc-store/generic-doc/by-period/{id}/index', methods: ['GET'])]
 | 
			
		||||
    public function __invoke(AccompanyingPeriod $accompanyingPeriod): JsonResponse
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)) {
 | 
			
		||||
            throw new AccessDeniedHttpException('not allowed to see the documents for accompanying period');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $nb = $this->manager->countDocForAccompanyingPeriod($accompanyingPeriod);
 | 
			
		||||
        $paginator = $this->paginator->create($nb);
 | 
			
		||||
 | 
			
		||||
        $docs = $this->manager->findDocForAccompanyingPeriod($accompanyingPeriod, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage());
 | 
			
		||||
 | 
			
		||||
        $collection = new Collection($docs, $paginator);
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse(
 | 
			
		||||
            $this->serializer->serialize($collection, 'json', [AbstractNormalizer::GROUPS => ['read']]),
 | 
			
		||||
            json: true,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,7 +11,7 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Manager;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
 | 
			
		||||
@@ -25,7 +25,7 @@ final readonly class GenericDocForPerson
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private FilterOrderHelperFactory $filterOrderHelperFactory,
 | 
			
		||||
        private Manager $manager,
 | 
			
		||||
        private ManagerInterface $manager,
 | 
			
		||||
        private PaginatorFactory $paginator,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private \Twig\Environment $twig,
 | 
			
		||||
 
 | 
			
		||||
@@ -46,9 +46,10 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: DocGeneratorTemplate::class)]
 | 
			
		||||
    private ?DocGeneratorTemplate $template = null;
 | 
			
		||||
 | 
			
		||||
    #[Assert\Length(min: 2, max: 250)]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
 | 
			
		||||
    private string $title = '';
 | 
			
		||||
    /**
 | 
			
		||||
     * Store the title of the document, if the title is set before the document.
 | 
			
		||||
     */
 | 
			
		||||
    private string $proxyTitle = '';
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: \Chill\MainBundle\Entity\User::class)]
 | 
			
		||||
    private ?\Chill\MainBundle\Entity\User $user = null;
 | 
			
		||||
@@ -78,9 +79,10 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
        return $this->template;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Assert\Length(min: 2, max: 250)]
 | 
			
		||||
    public function getTitle(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->title;
 | 
			
		||||
        return (string) $this->getObject()?->getTitle();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getUser()
 | 
			
		||||
@@ -113,6 +115,10 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
    {
 | 
			
		||||
        $this->object = $object;
 | 
			
		||||
 | 
			
		||||
        if ('' !== $this->proxyTitle) {
 | 
			
		||||
            $this->object->setTitle($this->proxyTitle);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -125,7 +131,11 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
 | 
			
		||||
    public function setTitle(string $title): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->title = $title;
 | 
			
		||||
        if (null !== $this->getObject()) {
 | 
			
		||||
            $this->getObject()->setTitle($title);
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->proxyTitle = $title;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\GenericDoc\Exception;
 | 
			
		||||
 | 
			
		||||
class AssociatedStoredObjectNotFound extends \RuntimeException
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(string $key, array $identifiers, int $code = 0, ?\Throwable $previous = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct(sprintf('No stored object found for generic doc with key "%s" and identifiers "%s"', $key, json_encode($identifiers)), $code, $previous);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\GenericDoc\Exception;
 | 
			
		||||
 | 
			
		||||
class NotNormalizableGenericDocException extends \LogicException {}
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\GenericDoc\Exception;
 | 
			
		||||
 | 
			
		||||
class UnexpectedValueException extends \UnexpectedValueException {}
 | 
			
		||||
@@ -13,7 +13,7 @@ namespace Chill\DocStoreBundle\GenericDoc;
 | 
			
		||||
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
 | 
			
		||||
interface GenericDocForAccompanyingPeriodProviderInterface
 | 
			
		||||
interface GenericDocForAccompanyingPeriodProviderInterface extends GenericDocProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    public function buildFetchQueryForAccompanyingPeriod(
 | 
			
		||||
        AccompanyingPeriod $accompanyingPeriod,
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\GenericDoc;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Normalize a Generic Doc.
 | 
			
		||||
 */
 | 
			
		||||
interface GenericDocNormalizerInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Return true if a generic doc can be normalized by this implementation.
 | 
			
		||||
     */
 | 
			
		||||
    public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Normalize a generic doc into an array.
 | 
			
		||||
     *
 | 
			
		||||
     * @return array{title: string, html?: string, isPresent: bool}
 | 
			
		||||
     */
 | 
			
		||||
    public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\GenericDoc;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
 | 
			
		||||
interface GenericDocProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return true if this provider supports the given Generic doc for various informations.
 | 
			
		||||
     *
 | 
			
		||||
     * Concerned:
 | 
			
		||||
     *
 | 
			
		||||
     * - @see{self::fetchAssociatedStoredObject}
 | 
			
		||||
     */
 | 
			
		||||
    public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * return true if the implementation supports key and identifiers.
 | 
			
		||||
     */
 | 
			
		||||
    public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Build a GenericDocDTO, given the key and identifiers.
 | 
			
		||||
     */
 | 
			
		||||
    public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO;
 | 
			
		||||
}
 | 
			
		||||
@@ -11,13 +11,16 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\GenericDoc;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Exception\AssociatedStoredObjectNotFound;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Exception\NotNormalizableGenericDocException;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\DBAL\Connection;
 | 
			
		||||
use Doctrine\DBAL\Exception;
 | 
			
		||||
use Doctrine\DBAL\Types\Types;
 | 
			
		||||
 | 
			
		||||
final readonly class Manager
 | 
			
		||||
final readonly class Manager implements ManagerInterface
 | 
			
		||||
{
 | 
			
		||||
    private FetchQueryToSqlBuilder $builder;
 | 
			
		||||
 | 
			
		||||
@@ -31,16 +34,16 @@ final readonly class Manager
 | 
			
		||||
         * @var iterable<GenericDocForPersonProviderInterface>
 | 
			
		||||
         */
 | 
			
		||||
        private iterable $providersForPerson,
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * @var iterable<GenericDocNormalizerInterface>
 | 
			
		||||
         */
 | 
			
		||||
        private iterable $genericDocNormalizers,
 | 
			
		||||
        private Connection $connection,
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->builder = new FetchQueryToSqlBuilder();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param list<string> $places
 | 
			
		||||
     *
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function countDocForAccompanyingPeriod(
 | 
			
		||||
        AccompanyingPeriod $accompanyingPeriod,
 | 
			
		||||
        ?\DateTimeImmutable $startDate = null,
 | 
			
		||||
@@ -83,13 +86,6 @@ final readonly class Manager
 | 
			
		||||
        return $this->countDoc($sql, $params, $types);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param list<string> $places places to search. When empty, search in all places
 | 
			
		||||
     *
 | 
			
		||||
     * @return iterable<GenericDocDTO>
 | 
			
		||||
     *
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function findDocForAccompanyingPeriod(
 | 
			
		||||
        AccompanyingPeriod $accompanyingPeriod,
 | 
			
		||||
        int $offset = 0,
 | 
			
		||||
@@ -129,10 +125,35 @@ final readonly class Manager
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param list<string> $places places to search. When empty, search in all places
 | 
			
		||||
     * Fetch a generic doc, if it does exists.
 | 
			
		||||
     *
 | 
			
		||||
     * @return iterable<GenericDocDTO>
 | 
			
		||||
     * Currently implemented only on generic docs linked with accompanying period
 | 
			
		||||
     */
 | 
			
		||||
    public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($this->providersForAccompanyingPeriod as $provider) {
 | 
			
		||||
            if ($provider->supportsKeyAndIdentifiers($key, $identifiers)) {
 | 
			
		||||
                return $provider->buildOneGenericDoc($key, $identifiers);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws AssociatedStoredObjectNotFound if no stored object can be found
 | 
			
		||||
     */
 | 
			
		||||
    public function fetchStoredObject(GenericDocDTO $genericDocDTO): StoredObject
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($this->providersForAccompanyingPeriod as $provider) {
 | 
			
		||||
            if ($provider->supportsGenericDoc($genericDocDTO)) {
 | 
			
		||||
                return $provider->fetchAssociatedStoredObject($genericDocDTO);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new AssociatedStoredObjectNotFound($genericDocDTO->key, $genericDocDTO->identifiers);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findDocForPerson(
 | 
			
		||||
        Person $person,
 | 
			
		||||
        int $offset = 0,
 | 
			
		||||
@@ -161,6 +182,28 @@ final readonly class Manager
 | 
			
		||||
        return $this->places($sql, $params, $types);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isGenericDocNormalizable(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($this->genericDocNormalizers as $genericDocNormalizer) {
 | 
			
		||||
            if ($genericDocNormalizer->supportsNormalization($genericDocDTO, $format, $context)) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function normalizeGenericDoc(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($this->genericDocNormalizers as $genericDocNormalizer) {
 | 
			
		||||
            if ($genericDocNormalizer->supportsNormalization($genericDocDTO, $format, $context)) {
 | 
			
		||||
                return $genericDocNormalizer->normalize($genericDocDTO, $format, $context);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new NotNormalizableGenericDocException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function places(string $sql, array $params, array $types): array
 | 
			
		||||
    {
 | 
			
		||||
        if ('' === $sql) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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\DocStoreBundle\GenericDoc;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Exception\AssociatedStoredObjectNotFound;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\DBAL\Exception;
 | 
			
		||||
 | 
			
		||||
interface ManagerInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @param list<string> $places
 | 
			
		||||
     *
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function countDocForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): int;
 | 
			
		||||
 | 
			
		||||
    public function countDocForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): int;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param list<string> $places places to search. When empty, search in all places
 | 
			
		||||
     *
 | 
			
		||||
     * @return iterable<GenericDocDTO>
 | 
			
		||||
     *
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function findDocForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, int $offset = 0, int $limit = 20, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): iterable;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param list<string> $places places to search. When empty, search in all places
 | 
			
		||||
     *
 | 
			
		||||
     * @return iterable<GenericDocDTO>
 | 
			
		||||
     */
 | 
			
		||||
    public function findDocForPerson(Person $person, int $offset = 0, int $limit = 20, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): iterable;
 | 
			
		||||
 | 
			
		||||
    public function placesForPerson(Person $person): array;
 | 
			
		||||
 | 
			
		||||
    public function placesForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): array;
 | 
			
		||||
 | 
			
		||||
    public function isGenericDocNormalizable(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return array{title: string, html?: string}
 | 
			
		||||
     */
 | 
			
		||||
    public function normalizeGenericDoc(GenericDocDTO $genericDocDTO, string $format, array $context = []): array;
 | 
			
		||||
 | 
			
		||||
    public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws AssociatedStoredObjectNotFound if no stored object can be found
 | 
			
		||||
     */
 | 
			
		||||
    public function fetchStoredObject(GenericDocDTO $genericDocDTO): StoredObject;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,56 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\GenericDoc\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Exception\UnexpectedValueException;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericDocProvider;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Renderer\AccompanyingCourseDocumentGenericDocRenderer;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
 | 
			
		||||
class AccompanyingCourseDocumentGenericDocNormalizer implements GenericDocNormalizerInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly AccompanyingCourseDocumentRepository $repository,
 | 
			
		||||
        private readonly Environment $twig,
 | 
			
		||||
        private readonly AccompanyingCourseDocumentGenericDocRenderer $renderer,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
 | 
			
		||||
    {
 | 
			
		||||
        return AccompanyingCourseDocumentGenericDocProvider::KEY === $genericDocDTO->key;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
 | 
			
		||||
    {
 | 
			
		||||
        if (!array_key_exists('id', $genericDocDTO->identifiers)) {
 | 
			
		||||
            throw new UnexpectedValueException('key id not found in identifier');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $document = $this->repository->find($genericDocDTO->identifiers['id']);
 | 
			
		||||
 | 
			
		||||
        if (null === $document) {
 | 
			
		||||
            throw new UnexpectedValueException('document not found with id '.$genericDocDTO->identifiers['id']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'isPresent' => true,
 | 
			
		||||
            'title' => $document->getTitle(),
 | 
			
		||||
            'html' => $this->twig->render(
 | 
			
		||||
                $this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
 | 
			
		||||
                $this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true])
 | 
			
		||||
            ),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\GenericDoc\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Renderer\AccompanyingCourseDocumentGenericDocRenderer;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
 | 
			
		||||
final readonly class PersonDocumentGenericDocNormalizer implements GenericDocNormalizerInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private PersonDocumentRepository $personDocumentRepository,
 | 
			
		||||
        private AccompanyingCourseDocumentGenericDocRenderer $renderer,
 | 
			
		||||
        private Environment $twig,
 | 
			
		||||
        private TranslatorInterface $translator,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
 | 
			
		||||
    {
 | 
			
		||||
        return PersonDocumentGenericDocProvider::KEY === $genericDocDTO->key && 'json' === $format;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $personDocument = $this->personDocumentRepository->find($genericDocDTO->identifiers['id'])) {
 | 
			
		||||
            return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'isPresent' => true,
 | 
			
		||||
            'title' => $personDocument->getTitle(),
 | 
			
		||||
            'html' => $this->twig->render(
 | 
			
		||||
                $this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
 | 
			
		||||
                $this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true])
 | 
			
		||||
            ),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,10 +12,13 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\DocStoreBundle\GenericDoc\Providers;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
@@ -31,17 +34,47 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
        private AccompanyingCourseDocumentRepository $accompanyingCourseDocumentRepository,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
 | 
			
		||||
    {
 | 
			
		||||
        return $this->accompanyingCourseDocumentRepository->find($genericDocDTO->identifiers['id'])?->getObject();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
 | 
			
		||||
    {
 | 
			
		||||
        return self::KEY === $key;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $accompanyingCourseDocument = $this->accompanyingCourseDocumentRepository->find($identifiers['id'])) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new GenericDocDTO(
 | 
			
		||||
            self::KEY,
 | 
			
		||||
            $identifiers,
 | 
			
		||||
            \DateTimeImmutable::createFromInterface($accompanyingCourseDocument->getDate()),
 | 
			
		||||
            $accompanyingCourseDocument->getCourse(),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
 | 
			
		||||
    {
 | 
			
		||||
        $classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);
 | 
			
		||||
 | 
			
		||||
        $query = new FetchQuery(
 | 
			
		||||
            self::KEY,
 | 
			
		||||
            sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]),
 | 
			
		||||
            sprintf('jsonb_build_object(\'id\', acc_course_document.%s)', $classMetadata->getIdentifierColumnNames()[0]),
 | 
			
		||||
            $classMetadata->getColumnName('date'),
 | 
			
		||||
            $classMetadata->getSchemaName().'.'.$classMetadata->getTableName()
 | 
			
		||||
            $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS acc_course_document'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $query->addWhereClause(
 | 
			
		||||
@@ -64,7 +97,7 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen
 | 
			
		||||
 | 
			
		||||
        $query = new FetchQuery(
 | 
			
		||||
            self::KEY,
 | 
			
		||||
            sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]),
 | 
			
		||||
            sprintf('jsonb_build_object(\'id\', acc_course_document.%s)', $classMetadata->getIdentifierColumnNames()[0]),
 | 
			
		||||
            $classMetadata->getColumnName('date'),
 | 
			
		||||
            $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS acc_course_document'
 | 
			
		||||
        );
 | 
			
		||||
@@ -110,6 +143,7 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen
 | 
			
		||||
    private function addWhereClause(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
 | 
			
		||||
    {
 | 
			
		||||
        $classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);
 | 
			
		||||
        $storedObjectMetadata = $this->entityManager->getClassMetadata(StoredObject::class);
 | 
			
		||||
 | 
			
		||||
        if (null !== $startDate) {
 | 
			
		||||
            $query->addWhereClause(
 | 
			
		||||
@@ -128,9 +162,19 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (null !== $content and '' !== $content) {
 | 
			
		||||
            // add join clause to stored_object table
 | 
			
		||||
            $query->addJoinClause(
 | 
			
		||||
                sprintf(
 | 
			
		||||
                    'JOIN %s AS doc_store ON doc_store.%s = acc_course_document.%s',
 | 
			
		||||
                    $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(),
 | 
			
		||||
                    $storedObjectMetadata->getSingleIdentifierColumnName(),
 | 
			
		||||
                    $classMetadata->getSingleAssociationJoinColumnName('object')
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            $query->addWhereClause(
 | 
			
		||||
                sprintf(
 | 
			
		||||
                    '(%s ilike ? OR %s ilike ?)',
 | 
			
		||||
                    '(doc_store.%s ilike ? OR acc_course_document.%s ilike ?)',
 | 
			
		||||
                    $classMetadata->getColumnName('title'),
 | 
			
		||||
                    $classMetadata->getColumnName('description')
 | 
			
		||||
                ),
 | 
			
		||||
 
 | 
			
		||||
@@ -11,10 +11,13 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\GenericDoc\Providers;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
@@ -27,8 +30,38 @@ final readonly class PersonDocumentGenericDocProvider implements GenericDocForPe
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository,
 | 
			
		||||
        private PersonDocumentRepository $personDocumentRepository,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
 | 
			
		||||
    {
 | 
			
		||||
        return $this->personDocumentRepository->find($genericDocDTO->identifiers['id'])?->getObject();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
 | 
			
		||||
    {
 | 
			
		||||
        return self::KEY === $key && array_key_exists('id', $identifiers);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $document = $this->personDocumentRepository->find($identifiers['id'])) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new GenericDocDTO(
 | 
			
		||||
            self::KEY,
 | 
			
		||||
            $identifiers,
 | 
			
		||||
            \DateTimeImmutable::createFromInterface($document->getDate()),
 | 
			
		||||
            $document->getPerson()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildFetchQueryForPerson(
 | 
			
		||||
        Person $person,
 | 
			
		||||
        ?\DateTimeImmutable $startDate = null,
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,9 @@ use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericD
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}>
 | 
			
		||||
 */
 | 
			
		||||
final readonly class AccompanyingCourseDocumentGenericDocRenderer implements GenericDocRendererInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
@@ -33,6 +36,10 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen
 | 
			
		||||
 | 
			
		||||
    public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
 | 
			
		||||
    {
 | 
			
		||||
        if ($options['row-only'] ?? false) {
 | 
			
		||||
            return '@ChillDocStore/List/list_item_row.html.twig';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return '@ChillDocStore/List/list_item.html.twig';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -44,6 +51,7 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen
 | 
			
		||||
                'accompanyingCourse' => $doc->getCourse(),
 | 
			
		||||
                'options' => $options,
 | 
			
		||||
                'context' => $genericDocDTO->getContext(),
 | 
			
		||||
                'show_actions' => $options['show-actions'] ?? true,
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -53,6 +61,7 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen
 | 
			
		||||
            'person' => $doc->getPerson(),
 | 
			
		||||
            'options' => $options,
 | 
			
		||||
            'context' => $genericDocDTO->getContext(),
 | 
			
		||||
            'show_actions' => $options['show-actions'] ?? true,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,11 +13,25 @@ namespace Chill\DocStoreBundle\GenericDoc\Twig;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Render a generic doc, to display it into a page.
 | 
			
		||||
 *
 | 
			
		||||
 * @template T of array
 | 
			
		||||
 */
 | 
			
		||||
interface GenericDocRendererInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @param T $options the options defined by the renderer
 | 
			
		||||
     */
 | 
			
		||||
    public function supports(GenericDocDTO $genericDocDTO, $options = []): bool;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param T $options the options defined by the renderer
 | 
			
		||||
     */
 | 
			
		||||
    public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param T $options the options defined by the renderer
 | 
			
		||||
     */
 | 
			
		||||
    public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\DocStoreBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\PersonDocument;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
 | 
			
		||||
@@ -136,6 +137,7 @@ final readonly class PersonDocumentACLAwareRepository implements PersonDocumentA
 | 
			
		||||
    private function addFilterClauses(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
 | 
			
		||||
    {
 | 
			
		||||
        $personDocMetadata = $this->em->getClassMetadata(PersonDocument::class);
 | 
			
		||||
        $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
 | 
			
		||||
 | 
			
		||||
        if (null !== $startDate) {
 | 
			
		||||
            $query->addWhereClause(
 | 
			
		||||
@@ -154,10 +156,20 @@ final readonly class PersonDocumentACLAwareRepository implements PersonDocumentA
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (null !== $content and '' !== $content) {
 | 
			
		||||
 | 
			
		||||
            $query->addJoinClause(
 | 
			
		||||
                sprintf(
 | 
			
		||||
                    'JOIN %s AS doc_store ON doc_store.%s = person_document.%s',
 | 
			
		||||
                    $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(),
 | 
			
		||||
                    $storedObjectMetadata->getSingleIdentifierColumnName(),
 | 
			
		||||
                    $personDocMetadata->getSingleAssociationJoinColumnName('object')
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            $query->addWhereClause(
 | 
			
		||||
                sprintf(
 | 
			
		||||
                    '(%s ilike ? OR %s ilike ?)',
 | 
			
		||||
                    $personDocMetadata->getColumnName('title'),
 | 
			
		||||
                    '(doc_store.%s ilike ? OR person_document.%s ilike ?)',
 | 
			
		||||
                    $storedObjectMetadata->getColumnName('title'),
 | 
			
		||||
                    $personDocMetadata->getColumnName('description')
 | 
			
		||||
                ),
 | 
			
		||||
                ['%'.$content.'%', '%'.$content.'%'],
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
 | 
			
		||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
 | 
			
		||||
export function fetch_generic_docs_by_accompanying_period(
 | 
			
		||||
    periodId: number,
 | 
			
		||||
): Promise<GenericDocForAccompanyingPeriod[]> {
 | 
			
		||||
    return fetchResults(
 | 
			
		||||
        `/api/1.0/doc-store/generic-doc/by-period/${periodId}/index`,
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { _createI18n } from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
 | 
			
		||||
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
 | 
			
		||||
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
 | 
			
		||||
import { createApp } from "vue";
 | 
			
		||||
import { StoredObject, StoredObjectStatusChange } from "../../types";
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,71 @@
 | 
			
		||||
import { DateTime } from "ChillMainAssets/types";
 | 
			
		||||
import { StoredObject } from "ChillDocStoreAssets/types/index";
 | 
			
		||||
 | 
			
		||||
export interface GenericDocMetadata {
 | 
			
		||||
    isPresent: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Empty metadata for a GenericDoc
 | 
			
		||||
 */
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
 | 
			
		||||
export interface EmptyMetadata extends GenericDocMetadata {}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Minimal Metadata for a GenericDoc with a normalizer
 | 
			
		||||
 */
 | 
			
		||||
export interface BaseMetadata extends GenericDocMetadata {
 | 
			
		||||
    title: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A generic doc is a document attached to a Person or an AccompanyingPeriod.
 | 
			
		||||
 */
 | 
			
		||||
export interface GenericDoc {
 | 
			
		||||
    type: "doc_store_generic_doc";
 | 
			
		||||
    uniqueKey: string;
 | 
			
		||||
    key: string;
 | 
			
		||||
    identifiers: object;
 | 
			
		||||
    context: "person" | "accompanying-period";
 | 
			
		||||
    doc_date: DateTime;
 | 
			
		||||
    metadata: GenericDocMetadata;
 | 
			
		||||
    storedObject: StoredObject | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenericDocForAccompanyingPeriod extends GenericDoc {
 | 
			
		||||
    context: "accompanying-period";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface BaseMetadataWithHtml extends BaseMetadata {
 | 
			
		||||
    html: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenericDocForAccompanyingCourseDocument
 | 
			
		||||
    extends GenericDocForAccompanyingPeriod {
 | 
			
		||||
    key: "accompanying_course_document";
 | 
			
		||||
    metadata: BaseMetadataWithHtml;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenericDocForAccompanyingCourseActivityDocument
 | 
			
		||||
    extends GenericDocForAccompanyingPeriod {
 | 
			
		||||
    key: "accompanying_course_activity_document";
 | 
			
		||||
    metadata: BaseMetadataWithHtml;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenericDocForAccompanyingCourseCalendarDocument
 | 
			
		||||
    extends GenericDocForAccompanyingPeriod {
 | 
			
		||||
    key: "accompanying_course_calendar_document";
 | 
			
		||||
    metadata: BaseMetadataWithHtml;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenericDocForAccompanyingCoursePersonDocument
 | 
			
		||||
    extends GenericDocForAccompanyingPeriod {
 | 
			
		||||
    key: "person_document";
 | 
			
		||||
    metadata: BaseMetadataWithHtml;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenericDocForAccompanyingCourseWorkEvaluationDocument
 | 
			
		||||
    extends GenericDocForAccompanyingPeriod {
 | 
			
		||||
    key: "accompanying_period_work_evaluation_document";
 | 
			
		||||
    metadata: BaseMetadataWithHtml;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +1,5 @@
 | 
			
		||||
import {
 | 
			
		||||
    DateTime,
 | 
			
		||||
    User,
 | 
			
		||||
} from "../../../ChillMainBundle/Resources/public/types";
 | 
			
		||||
import { SignedUrlGet } from "./vuejs/StoredObjectButton/helpers";
 | 
			
		||||
import { DateTime, User } from "ChillMainAssets/types";
 | 
			
		||||
import { SignedUrlGet } from "ChillDocStoreAssets/vuejs/StoredObjectButton/helpers";
 | 
			
		||||
 | 
			
		||||
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
 | 
			
		||||
 | 
			
		||||
@@ -138,3 +135,10 @@ export interface ZoomLevel {
 | 
			
		||||
        nl?: string;
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenericDoc {
 | 
			
		||||
    type: "doc_store_generic_doc";
 | 
			
		||||
    key: string;
 | 
			
		||||
    context: "person" | "accompanying-period";
 | 
			
		||||
    doc_date: DateTime;
 | 
			
		||||
}
 | 
			
		||||
@@ -66,7 +66,7 @@ const open_button = ref<HTMLAnchorElement | null>(null);
 | 
			
		||||
function buildDocumentName(): string {
 | 
			
		||||
    let document_name = props.filename ?? props.storedObject.title;
 | 
			
		||||
 | 
			
		||||
    if ("" === document_name) {
 | 
			
		||||
    if ("" === document_name || null === document_name) {
 | 
			
		||||
        document_name = "document";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,120 +1,3 @@
 | 
			
		||||
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
 | 
			
		||||
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
 | 
			
		||||
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
 | 
			
		||||
 | 
			
		||||
<div class="item-bloc">
 | 
			
		||||
    <div class="item-row">
 | 
			
		||||
        <div class="item-col" style="width: unset">
 | 
			
		||||
            {% if document.object.isPending %}
 | 
			
		||||
                <div class="badge text-bg-info" data-docgen-is-pending="{{ document.object.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
 | 
			
		||||
            {% elseif document.object.isFailure %}
 | 
			
		||||
                <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 | 
			
		||||
            {% if context == 'person' and accompanyingCourse is defined %}
 | 
			
		||||
                <div>
 | 
			
		||||
                    <span class="badge bg-primary">
 | 
			
		||||
                        <i class="fa fa-random"></i> {{ accompanyingCourse.id }}
 | 
			
		||||
                    </span> 
 | 
			
		||||
                </div>
 | 
			
		||||
                {% elseif context == 'accompanying-period' and person is defined %}
 | 
			
		||||
                    <div>
 | 
			
		||||
                    <span class="badge bg-primary">
 | 
			
		||||
                        {{  'Document from person %name%'|trans({ '%name%': document.person|chill_entity_render_string }) }}
 | 
			
		||||
                    </span> 
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            <div class="denomination h2">
 | 
			
		||||
                {{ document.title|chill_print_or_message("No title") }}
 | 
			
		||||
            </div>
 | 
			
		||||
            {% if document.object.type is not empty %}
 | 
			
		||||
                <div>
 | 
			
		||||
                    {{ mm.mimeIcon(document.object.type) }}
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            <div>
 | 
			
		||||
                <p>{{ document.category.name|localize_translatable_string }}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% if document.object.hasTemplate %}
 | 
			
		||||
                <div>
 | 
			
		||||
                    <p>{{ document.object.template.name|localize_translatable_string }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="item-col">
 | 
			
		||||
            <div class="container">
 | 
			
		||||
                {% if document.date is not null %}
 | 
			
		||||
                    <div class="dates row text-end">
 | 
			
		||||
                        <span>{{ document.date|format_date('short') }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% if document.description is not empty %}
 | 
			
		||||
        <div class="item-row">
 | 
			
		||||
            <blockquote class="chill-user-quote col">
 | 
			
		||||
                {{ document.description|chill_markdown_to_html }}
 | 
			
		||||
            </blockquote>
 | 
			
		||||
        </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    <div class="item-row separator">
 | 
			
		||||
        <div class="item-col item-meta">
 | 
			
		||||
            {{ mmm.createdBy(document) }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <ul class="item-col record_actions flex-shrink-1">
 | 
			
		||||
            {% if document.course is defined %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    {{ chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) }}
 | 
			
		||||
                </li>
 | 
			
		||||
                {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        {{ document.object|chill_document_button_group(document.title) }}
 | 
			
		||||
                    </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
 | 
			
		||||
                    <li class="delete">
 | 
			
		||||
                        <a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', document.course) %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a href="{{ chill_path_add_return_path('chill_doc_store_accompanying_course_document_duplicate', {'id': document.id}) }}" class="btn btn-duplicate" title="{{ 'Duplicate'|trans|e('html_attr') }}"></a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            {% else %}
 | 
			
		||||
                {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        {{ document.object|chill_document_button_group(document.title) }}
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a href="{{ path('person_document_edit', {'person': person.id, 'id': document.id}) }}" class="btn btn-update"></a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if is_granted('CHILL_PERSON_DOCUMENT_DELETE', document) %}
 | 
			
		||||
                    <li class="delete">
 | 
			
		||||
                        <a href="{{ chill_return_path_or('chill_docstore_person_document_delete', {'person': person.id, 'id': document.id}) }}" class="btn btn-delete"></a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </ul>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
   {% include '@ChillDocStore/List/list_item_row.html.twig'%}
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,119 @@
 | 
			
		||||
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
 | 
			
		||||
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
 | 
			
		||||
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
 | 
			
		||||
 | 
			
		||||
<div class="item-row">
 | 
			
		||||
    <div class="item-col" style="width: unset">
 | 
			
		||||
        {% if document.object.isPending %}
 | 
			
		||||
            <div class="badge text-bg-info" data-docgen-is-pending="{{ document.object.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
 | 
			
		||||
        {% elseif document.object.isFailure %}
 | 
			
		||||
            <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        {% if context == 'person' and accompanyingCourse is defined %}
 | 
			
		||||
            <div>
 | 
			
		||||
                    <span class="badge bg-primary">
 | 
			
		||||
                        <i class="fa fa-random"></i> {{ accompanyingCourse.id }}
 | 
			
		||||
                    </span> 
 | 
			
		||||
            </div>
 | 
			
		||||
        {% elseif context == 'accompanying-period' and person is defined %}
 | 
			
		||||
            <div>
 | 
			
		||||
                    <span class="badge bg-primary">
 | 
			
		||||
                        {{  'Document from person %name%'|trans({ '%name%': document.person|chill_entity_render_string }) }}
 | 
			
		||||
                    </span> 
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        <div class="denomination h2">
 | 
			
		||||
            {{ document.title|chill_print_or_message("No title") }}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if document.object.type is not empty %}
 | 
			
		||||
            <div>
 | 
			
		||||
                {{ mm.mimeIcon(document.object.type) }}
 | 
			
		||||
            </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        <div>
 | 
			
		||||
            <p>{{ document.category.name|localize_translatable_string }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if document.object.hasTemplate %}
 | 
			
		||||
            <div>
 | 
			
		||||
                <p>{{ document.object.template.name|localize_translatable_string }}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="item-col">
 | 
			
		||||
        <div class="container">
 | 
			
		||||
            {% if document.date is not null %}
 | 
			
		||||
                <div class="dates row text-end">
 | 
			
		||||
                    <span>{{ document.date|format_date('short') }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% if document.description is not empty %}
 | 
			
		||||
    <div class="item-row">
 | 
			
		||||
        <blockquote class="chill-user-quote col">
 | 
			
		||||
            {{ document.description|chill_markdown_to_html }}
 | 
			
		||||
        </blockquote>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
    {% if show_actions %}
 | 
			
		||||
        <div class="item-row separator">
 | 
			
		||||
            <div class="item-col item-meta">
 | 
			
		||||
                {{ mmm.createdBy(document) }}
 | 
			
		||||
            </div>
 | 
			
		||||
            <ul class="item-col record_actions flex-shrink-1">
 | 
			
		||||
                {% if document.course is defined %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        {{ chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) }}
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
 | 
			
		||||
                        <li>
 | 
			
		||||
                            {{ document.object|chill_document_button_group(document.title) }}
 | 
			
		||||
                        </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
 | 
			
		||||
                        <li class="delete">
 | 
			
		||||
                            <a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', document.course) %}
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <a href="{{ chill_path_add_return_path('chill_doc_store_accompanying_course_document_duplicate', {'id': document.id}) }}" class="btn btn-duplicate" title="{{ 'Duplicate'|trans|e('html_attr') }}"></a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                {% else %}
 | 
			
		||||
                    {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
 | 
			
		||||
                        <li>
 | 
			
		||||
                            {{ document.object|chill_document_button_group(document.title) }}
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %}
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <a href="{{ path('person_document_edit', {'person': person.id, 'id': document.id}) }}" class="btn btn-update"></a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if is_granted('CHILL_PERSON_DOCUMENT_DELETE', document) %}
 | 
			
		||||
                        <li class="delete">
 | 
			
		||||
                            <a href="{{ chill_return_path_or('chill_docstore_person_document_delete', {'person': person.id, 'id': document.id}) }}" class="btn btn-delete"></a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
@@ -24,9 +24,9 @@
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <div class="row">
 | 
			
		||||
    <div class="row g-3">
 | 
			
		||||
        <div class="col-xs-12 col-sm-6 col-md-4">
 | 
			
		||||
            <div class="card"">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h2 class="card-title">{{ title }}</h2>
 | 
			
		||||
                    <h3>{{ 'workflow.public_link.main_document'|trans }}</h3>
 | 
			
		||||
@@ -39,5 +39,21 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% for attachment in attachments %}
 | 
			
		||||
            <div class="col-xs-12 col-sm-6 col-md-4">
 | 
			
		||||
                <div class="card">
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <h2 class="card-title">{{ attachment.proxyStoredObject.title }}</h2>
 | 
			
		||||
                        <h3>{{ 'workflow.public_link.attachment'|trans }}</h3>
 | 
			
		||||
 | 
			
		||||
                        <ul class="record_actions slim small">
 | 
			
		||||
                            <li>
 | 
			
		||||
                                {{ attachment.proxyStoredObject|chill_document_download_only_button(storedObject.title(), false) }}
 | 
			
		||||
                            </li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,8 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
 | 
			
		||||
@@ -26,7 +28,12 @@ class StoredObjectVoter extends Voter
 | 
			
		||||
{
 | 
			
		||||
    public const LOG_PREFIX = '[stored object voter] ';
 | 
			
		||||
 | 
			
		||||
    public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters, private readonly LoggerInterface $logger) {}
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly Security $security,
 | 
			
		||||
        private readonly iterable $storedObjectVoters,
 | 
			
		||||
        private readonly LoggerInterface $logger,
 | 
			
		||||
        private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    protected function supports($attribute, $subject): bool
 | 
			
		||||
    {
 | 
			
		||||
@@ -39,6 +46,16 @@ class StoredObjectVoter extends Voter
 | 
			
		||||
        /** @var StoredObject $subject */
 | 
			
		||||
        $attributeAsEnum = StoredObjectRoleEnum::from($attribute);
 | 
			
		||||
 | 
			
		||||
        // check if the stored object is attached to any workflow
 | 
			
		||||
        $user = $token->getUser();
 | 
			
		||||
        if ($user instanceof User && StoredObjectRoleEnum::SEE === $attributeAsEnum) {
 | 
			
		||||
            foreach ($this->entityWorkflowAttachmentRepository->findByStoredObject($subject) as $workflowAttachment) {
 | 
			
		||||
                if ($workflowAttachment->getEntityWorkflow()->isUserInvolved($user)) {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Loop through context-specific voters
 | 
			
		||||
        foreach ($this->storedObjectVoters as $storedObjectVoter) {
 | 
			
		||||
            if ($storedObjectVoter->supports($attributeAsEnum, $subject)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,67 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Serializer\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Exception\AssociatedStoredObjectNotFound;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
 | 
			
		||||
class GenericDocNormalizer implements NormalizerInterface, NormalizerAwareInterface
 | 
			
		||||
{
 | 
			
		||||
    use NormalizerAwareTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Special key to attach a stored object to the generic doc.
 | 
			
		||||
     *
 | 
			
		||||
     * This is present for performance reason: if any other part of the application "knows" about the stored object
 | 
			
		||||
     * related to the GenericDoc, this stored object is use instead of adding costly sql queries.
 | 
			
		||||
     */
 | 
			
		||||
    public const ATTACHED_STORED_OBJECT_PROXY = 'attached-stored-object-proxy';
 | 
			
		||||
 | 
			
		||||
    public function __construct(private readonly ManagerInterface $manager) {}
 | 
			
		||||
 | 
			
		||||
    public function normalize($object, ?string $format = null, array $context = []): array
 | 
			
		||||
    {
 | 
			
		||||
        /* @var GenericDocDTO $object */
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $storedObject = $context[self::ATTACHED_STORED_OBJECT_PROXY] ?? $this->manager->fetchStoredObject($object);
 | 
			
		||||
        } catch (AssociatedStoredObjectNotFound) {
 | 
			
		||||
            $storedObject = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $data = [
 | 
			
		||||
            'type' => 'doc_store_generic_doc',
 | 
			
		||||
            'key' => $object->key,
 | 
			
		||||
            'uniqueKey' => $object->key.implode('', array_keys($object->identifiers)).implode('', array_values($object->identifiers)),
 | 
			
		||||
            'identifiers' => $object->identifiers,
 | 
			
		||||
            'context' => $object->getContext(),
 | 
			
		||||
            'doc_date' => $this->normalizer->normalize($object->docDate, $format, $context),
 | 
			
		||||
            'metadata' => [],
 | 
			
		||||
            'storedObject' => $this->normalizer->normalize($storedObject, $format, $context),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if ($this->manager->isGenericDocNormalizable($object, $format, $context)) {
 | 
			
		||||
            $data['metadata'] = $this->manager->normalizeGenericDoc($object, $format, $context);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsNormalization($data, ?string $format = null): bool
 | 
			
		||||
    {
 | 
			
		||||
        return 'json' === $format && $data instanceof GenericDocDTO;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,10 +11,13 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Tests\GenericDoc;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Exception\NotNormalizableGenericDocException;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Manager;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
@@ -58,6 +61,7 @@ class ManagerTest extends KernelTestCase
 | 
			
		||||
        $manager = new Manager(
 | 
			
		||||
            [new SimpleGenericDocAccompanyingPeriodProvider()],
 | 
			
		||||
            [new SimpleGenericDocPersonProvider()],
 | 
			
		||||
            [],
 | 
			
		||||
            $this->connection,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@@ -79,6 +83,7 @@ class ManagerTest extends KernelTestCase
 | 
			
		||||
        $manager = new Manager(
 | 
			
		||||
            [new SimpleGenericDocAccompanyingPeriodProvider()],
 | 
			
		||||
            [new SimpleGenericDocPersonProvider()],
 | 
			
		||||
            [],
 | 
			
		||||
            $this->connection,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@@ -100,6 +105,7 @@ class ManagerTest extends KernelTestCase
 | 
			
		||||
        $manager = new Manager(
 | 
			
		||||
            [new SimpleGenericDocAccompanyingPeriodProvider()],
 | 
			
		||||
            [new SimpleGenericDocPersonProvider()],
 | 
			
		||||
            [],
 | 
			
		||||
            $this->connection,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@@ -121,6 +127,7 @@ class ManagerTest extends KernelTestCase
 | 
			
		||||
        $manager = new Manager(
 | 
			
		||||
            [new SimpleGenericDocAccompanyingPeriodProvider()],
 | 
			
		||||
            [new SimpleGenericDocPersonProvider()],
 | 
			
		||||
            [],
 | 
			
		||||
            $this->connection,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@@ -142,6 +149,7 @@ class ManagerTest extends KernelTestCase
 | 
			
		||||
        $manager = new Manager(
 | 
			
		||||
            [new SimpleGenericDocAccompanyingPeriodProvider()],
 | 
			
		||||
            [new SimpleGenericDocPersonProvider()],
 | 
			
		||||
            [],
 | 
			
		||||
            $this->connection,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@@ -163,6 +171,7 @@ class ManagerTest extends KernelTestCase
 | 
			
		||||
        $manager = new Manager(
 | 
			
		||||
            [new SimpleGenericDocAccompanyingPeriodProvider()],
 | 
			
		||||
            [new SimpleGenericDocPersonProvider()],
 | 
			
		||||
            [],
 | 
			
		||||
            $this->connection,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@@ -170,10 +179,77 @@ class ManagerTest extends KernelTestCase
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(['accompanying_course_document_dummy'], $places);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testIsGenericDocNormalizable(): void
 | 
			
		||||
    {
 | 
			
		||||
        $genericDoc = new GenericDocDTO('test', ['id' => 1], new \DateTimeImmutable(), new AccompanyingPeriod());
 | 
			
		||||
 | 
			
		||||
        $manager = new Manager([], [], [$this->buildNormalizer(true)], $this->connection);
 | 
			
		||||
        self::assertTrue($manager->isGenericDocNormalizable($genericDoc, 'json'));
 | 
			
		||||
 | 
			
		||||
        $manager = new Manager([], [], [$this->buildNormalizer(false)], $this->connection);
 | 
			
		||||
        self::assertFalse($manager->isGenericDocNormalizable($genericDoc, 'json'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testNormalizeGenericDocMetadata(): void
 | 
			
		||||
    {
 | 
			
		||||
        $genericDoc = new GenericDocDTO('test', ['id' => 1], new \DateTimeImmutable(), new AccompanyingPeriod());
 | 
			
		||||
 | 
			
		||||
        $manager = new Manager([], [], [$this->buildNormalizer(false), $this->buildNormalizer(true)], $this->connection);
 | 
			
		||||
        self::assertEquals(['title' => 'Some title'], $manager->normalizeGenericDoc($genericDoc, 'json'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testNormalizeGenericDocMetadataNoNormalizer(): void
 | 
			
		||||
    {
 | 
			
		||||
        $genericDoc = new GenericDocDTO('test', ['id' => 1], new \DateTimeImmutable(), new AccompanyingPeriod());
 | 
			
		||||
 | 
			
		||||
        $manager = new Manager([], [], [$this->buildNormalizer(false)], $this->connection);
 | 
			
		||||
 | 
			
		||||
        $this->expectException(NotNormalizableGenericDocException::class);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(['title' => 'Some title'], $manager->normalizeGenericDoc($genericDoc, 'json'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildNormalizer(bool $supports): GenericDocNormalizerInterface
 | 
			
		||||
    {
 | 
			
		||||
        return new class ($supports) implements GenericDocNormalizerInterface {
 | 
			
		||||
            public function __construct(private readonly bool $supports) {}
 | 
			
		||||
 | 
			
		||||
            public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
 | 
			
		||||
            {
 | 
			
		||||
                return $this->supports;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
 | 
			
		||||
            {
 | 
			
		||||
                return ['title' => 'Some title'];
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final readonly class SimpleGenericDocAccompanyingPeriodProvider implements GenericDocForAccompanyingPeriodProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
 | 
			
		||||
    {
 | 
			
		||||
        throw new \BadMethodCallException('not implemented');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
 | 
			
		||||
    {
 | 
			
		||||
        throw new \BadMethodCallException('not implemented');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
 | 
			
		||||
    {
 | 
			
		||||
        return 'accompanying_course_document_dummy' === $key;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
 | 
			
		||||
    {
 | 
			
		||||
        return new GenericDocDTO('accompanying_course_document_dummy', $identifiers, new \DateTimeImmutable(), new AccompanyingPeriod());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
 | 
			
		||||
    {
 | 
			
		||||
        $query = new FetchQuery(
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ namespace Chill\DocStoreBundle\Tests\GenericDoc\Providers;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericDocProvider;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
@@ -56,7 +57,8 @@ class AccompanyingCourseDocumentGenericDocProviderTest extends KernelTestCase
 | 
			
		||||
 | 
			
		||||
        $provider = new AccompanyingCourseDocumentGenericDocProvider(
 | 
			
		||||
            $security->reveal(),
 | 
			
		||||
            $this->entityManager
 | 
			
		||||
            $this->entityManager,
 | 
			
		||||
            $this->prophesize(AccompanyingCourseDocumentRepository::class)->reveal()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $query = $provider->buildFetchQueryForAccompanyingPeriod($period, $startDate, $endDate, $content);
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\Tests\GenericDoc\Providers;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
@@ -33,11 +34,14 @@ class PersonDocumentGenericDocProviderTest extends KernelTestCase
 | 
			
		||||
 | 
			
		||||
    private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository;
 | 
			
		||||
 | 
			
		||||
    private PersonDocumentRepository $personDocumentRepository;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
        $this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
 | 
			
		||||
        $this->personDocumentACLAwareRepository = self::getContainer()->get(PersonDocumentACLAwareRepositoryInterface::class);
 | 
			
		||||
        $this->personDocumentRepository = self::getContainer()->get(PersonDocumentRepository::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -60,7 +64,8 @@ class PersonDocumentGenericDocProviderTest extends KernelTestCase
 | 
			
		||||
 | 
			
		||||
        $provider = new PersonDocumentGenericDocProvider(
 | 
			
		||||
            $security->reveal(),
 | 
			
		||||
            $this->personDocumentACLAwareRepository
 | 
			
		||||
            $this->personDocumentACLAwareRepository,
 | 
			
		||||
            $this->personDocumentRepository,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $query = $provider->buildFetchQueryForPerson($person, $startDate, $endDate, $content);
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Psr\Log\NullLogger;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
@@ -44,7 +45,7 @@ class StoredObjectVoterTest extends TestCase
 | 
			
		||||
            ->with($this->logicalOr($this->identicalTo('ROLE_USER'), $this->identicalTo('ROLE_ADMIN')))
 | 
			
		||||
            ->willReturn($securityIsGrantedResult);
 | 
			
		||||
 | 
			
		||||
        $voter = new StoredObjectVoter($security, $storedObjectVoters, new NullLogger());
 | 
			
		||||
        $voter = new StoredObjectVoter($security, $storedObjectVoters, new NullLogger(), $this->createMock(EntityWorkflowAttachmentRepository::class));
 | 
			
		||||
 | 
			
		||||
        self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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\DocGeneratorBundle\Tests\Serializer\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
 | 
			
		||||
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Serializer\Normalizer\GenericDocNormalizer;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class GenericDocNormalizerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    private $normalizer;
 | 
			
		||||
 | 
			
		||||
    private ManagerInterface $manager;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->manager = $this->createMock(ManagerInterface::class);
 | 
			
		||||
 | 
			
		||||
        $this->normalizer = new GenericDocNormalizer($this->manager);
 | 
			
		||||
 | 
			
		||||
        $innerNormalizer = $this->createMock(NormalizerInterface::class);
 | 
			
		||||
        $innerNormalizer->method('normalize')
 | 
			
		||||
            ->willReturnCallback(fn ($date) => $date instanceof \DateTimeImmutable ? $date->format(DATE_ATOM) : null);
 | 
			
		||||
 | 
			
		||||
        $this->normalizer->setNormalizer($innerNormalizer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testNormalize()
 | 
			
		||||
    {
 | 
			
		||||
        $docDate = new \DateTimeImmutable('2023-10-01T15:03:01.012345Z');
 | 
			
		||||
 | 
			
		||||
        $object = new GenericDocDTO(
 | 
			
		||||
            'some_key',
 | 
			
		||||
            ['id' => 'id1', 'other_id' => 'id2'],
 | 
			
		||||
            $docDate,
 | 
			
		||||
            new AccompanyingPeriod(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $expected = [
 | 
			
		||||
            'type' => 'doc_store_generic_doc',
 | 
			
		||||
            'key' => 'some_key',
 | 
			
		||||
            'identifiers' => ['id' => 'id1', 'other_id' => 'id2'],
 | 
			
		||||
            'context' => 'accompanying-period',
 | 
			
		||||
            'doc_date' => $docDate->format(DATE_ATOM),
 | 
			
		||||
            'uniqueKey' => 'some_keyidother_idid1id2',
 | 
			
		||||
            'metadata' => [],
 | 
			
		||||
            'storedObject' => null,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $this->manager->expects($this->once())->method('isGenericDocNormalizable')
 | 
			
		||||
            ->with($object, 'json', [])
 | 
			
		||||
            ->willReturn(true);
 | 
			
		||||
 | 
			
		||||
        $actual = $this->normalizer->normalize($object, 'json', []);
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals($expected, $actual);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,6 +21,7 @@ use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
 | 
			
		||||
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
 | 
			
		||||
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvideThirdPartiesAssociated;
 | 
			
		||||
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvidePersonsAssociated;
 | 
			
		||||
@@ -69,7 +70,7 @@ final readonly class AccompanyingCourseDocumentWorkflowHandler implements Entity
 | 
			
		||||
            return $this->translator->trans('workflow.Document deleted');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->translator->trans('workflow.Document (n°%doc%)', ['%doc%' => $entityWorkflow->getRelatedEntityId()])
 | 
			
		||||
        return $this->translator->trans('entity_display_title.Document (n°%doc%)', ['%doc%' => $entityWorkflow->getRelatedEntityId()])
 | 
			
		||||
            .' - '.$doc->getTitle();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -78,6 +79,15 @@ final readonly class AccompanyingCourseDocumentWorkflowHandler implements Entity
 | 
			
		||||
        return $this->repository->find($entityWorkflow->getRelatedEntityId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getRelatedAccompanyingPeriod(EntityWorkflow $entityWorkflow): ?AccompanyingPeriod
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $document = $this->getRelatedEntity($entityWorkflow)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $document->getCourse();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return array[]
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,7 @@ class WorkflowWithPublicViewDocumentHelper
 | 
			
		||||
                'storedObject' => $storedObject,
 | 
			
		||||
                'send' => $send,
 | 
			
		||||
                'metadata' => $metadata,
 | 
			
		||||
                'attachments' => $entityWorkflow->getAttachments(),
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,22 @@ components:
 | 
			
		||||
                    type: string
 | 
			
		||||
                type:
 | 
			
		||||
                    type: string
 | 
			
		||||
        GenericDoc:
 | 
			
		||||
            type: object
 | 
			
		||||
            properties:
 | 
			
		||||
                type:
 | 
			
		||||
                    type: string
 | 
			
		||||
                    enum:
 | 
			
		||||
                        - doc_store_generic_doc
 | 
			
		||||
                key:
 | 
			
		||||
                    type: string
 | 
			
		||||
                context:
 | 
			
		||||
                    type: string
 | 
			
		||||
                    enum:
 | 
			
		||||
                        - person
 | 
			
		||||
                        - accompanying-period
 | 
			
		||||
                doc_date:
 | 
			
		||||
                    $ref: '#/components/schemas/Date'
 | 
			
		||||
 | 
			
		||||
paths:
 | 
			
		||||
    /1.0/doc-store/stored-object/create:
 | 
			
		||||
@@ -69,30 +85,30 @@ paths:
 | 
			
		||||
                - storedobject
 | 
			
		||||
            summary: Get a signed route to get a stored object
 | 
			
		||||
            parameters:
 | 
			
		||||
                - in: path
 | 
			
		||||
                  name: uuid
 | 
			
		||||
                  required: true
 | 
			
		||||
                  allowEmptyValue: false
 | 
			
		||||
                  description: The UUID of the storedObjeect
 | 
			
		||||
                  schema:
 | 
			
		||||
                      type: string
 | 
			
		||||
                      format: uuid
 | 
			
		||||
                - in: path
 | 
			
		||||
                  name: method
 | 
			
		||||
                  required: true
 | 
			
		||||
                  allowEmptyValue: false
 | 
			
		||||
                  description: the method of the signed url (get or head)
 | 
			
		||||
                  schema:
 | 
			
		||||
                      type: string
 | 
			
		||||
                      enum: [get, head]
 | 
			
		||||
                - in: query
 | 
			
		||||
                  name: version
 | 
			
		||||
                  required: false
 | 
			
		||||
                  allowEmptyValue: false
 | 
			
		||||
                  description: the version's filename of the stored object
 | 
			
		||||
                  schema:
 | 
			
		||||
                      type: string
 | 
			
		||||
                      minLength: 2
 | 
			
		||||
                -   in: path
 | 
			
		||||
                    name: uuid
 | 
			
		||||
                    required: true
 | 
			
		||||
                    allowEmptyValue: false
 | 
			
		||||
                    description: The UUID of the storedObjeect
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: string
 | 
			
		||||
                        format: uuid
 | 
			
		||||
                -   in: path
 | 
			
		||||
                    name: method
 | 
			
		||||
                    required: true
 | 
			
		||||
                    allowEmptyValue: false
 | 
			
		||||
                    description: the method of the signed url (get or head)
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: string
 | 
			
		||||
                        enum: [ get, head ]
 | 
			
		||||
                -   in: query
 | 
			
		||||
                    name: version
 | 
			
		||||
                    required: false
 | 
			
		||||
                    allowEmptyValue: false
 | 
			
		||||
                    description: the version's filename of the stored object
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: string
 | 
			
		||||
                        minLength: 2
 | 
			
		||||
            responses:
 | 
			
		||||
                200:
 | 
			
		||||
                    description: "OK"
 | 
			
		||||
@@ -111,14 +127,14 @@ paths:
 | 
			
		||||
                - storedobject
 | 
			
		||||
            summary: Get a signed route to post stored object
 | 
			
		||||
            parameters:
 | 
			
		||||
                - in: path
 | 
			
		||||
                  name: uuid
 | 
			
		||||
                  required: true
 | 
			
		||||
                  allowEmptyValue: false
 | 
			
		||||
                  description: The UUID of the storedObjeect
 | 
			
		||||
                  schema:
 | 
			
		||||
                      type: string
 | 
			
		||||
                      format: uuid
 | 
			
		||||
                -   in: path
 | 
			
		||||
                    name: uuid
 | 
			
		||||
                    required: true
 | 
			
		||||
                    allowEmptyValue: false
 | 
			
		||||
                    description: The UUID of the storedObjeect
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: string
 | 
			
		||||
                        format: uuid
 | 
			
		||||
            responses:
 | 
			
		||||
                200:
 | 
			
		||||
                    description: "OK"
 | 
			
		||||
@@ -137,13 +153,13 @@ paths:
 | 
			
		||||
                - storedobject
 | 
			
		||||
            summary: Restore an old version of a stored object
 | 
			
		||||
            parameters:
 | 
			
		||||
                - in: path
 | 
			
		||||
                  name: id
 | 
			
		||||
                  required: true
 | 
			
		||||
                  allowEmptyValue: false
 | 
			
		||||
                  description: The id of the stored object version
 | 
			
		||||
                  schema:
 | 
			
		||||
                      type: integer
 | 
			
		||||
                -   in: path
 | 
			
		||||
                    name: id
 | 
			
		||||
                    required: true
 | 
			
		||||
                    allowEmptyValue: false
 | 
			
		||||
                    description: The id of the stored object version
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: integer
 | 
			
		||||
            responses:
 | 
			
		||||
                200:
 | 
			
		||||
                    description: "OK"
 | 
			
		||||
@@ -151,4 +167,32 @@ paths:
 | 
			
		||||
                        application/json:
 | 
			
		||||
                            schema:
 | 
			
		||||
                                type: object
 | 
			
		||||
    /1.0/doc-store/generic-doc/by-period/{id}/index:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
                - storedobject
 | 
			
		||||
            summary: A list of generic doc associated with the accompanying period
 | 
			
		||||
            parameters:
 | 
			
		||||
                -   in: path
 | 
			
		||||
                    name: id
 | 
			
		||||
                    required: true
 | 
			
		||||
                    allowEmptyValue: false
 | 
			
		||||
                    description: The id of the accompanying period
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: integer
 | 
			
		||||
            responses:
 | 
			
		||||
                200:
 | 
			
		||||
                    description: "OK"
 | 
			
		||||
                    content:
 | 
			
		||||
                        application/json:
 | 
			
		||||
                            schema:
 | 
			
		||||
                                allOf:
 | 
			
		||||
                                    - $ref: '#/components/schemas/PaginatedResult'
 | 
			
		||||
                                    - type: object
 | 
			
		||||
                                      properties:
 | 
			
		||||
                                          results:
 | 
			
		||||
                                              type: array
 | 
			
		||||
                                              items:
 | 
			
		||||
                                                  $ref: '#/components/schemas/GenericDoc'
 | 
			
		||||
 | 
			
		||||
                                type: object
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,10 @@ services:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $providersForAccompanyingPeriod: !tagged_iterator chill_doc_store.generic_doc_accompanying_period_provider
 | 
			
		||||
            $providersForPerson: !tagged_iterator chill_doc_store.generic_doc_person_provider
 | 
			
		||||
            $genericDocNormalizers: !tagged_iterator chill_doc_store.generic_doc_metadata_normalizer
 | 
			
		||||
 | 
			
		||||
    Chill\DocStoreBundle\GenericDoc\ManagerInterface:
 | 
			
		||||
        alias: Chill\DocStoreBundle\GenericDoc\Manager
 | 
			
		||||
 | 
			
		||||
    Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension: ~
 | 
			
		||||
 | 
			
		||||
@@ -44,6 +48,9 @@ services:
 | 
			
		||||
    Chill\DocStoreBundle\GenericDoc\Renderer\:
 | 
			
		||||
        resource: '../GenericDoc/Renderer/'
 | 
			
		||||
 | 
			
		||||
    Chill\DocStoreBundle\GenericDoc\Normalizer\:
 | 
			
		||||
        resource: '../GenericDoc/Normalizer/'
 | 
			
		||||
 | 
			
		||||
    Chill\DocStoreBundle\Validator\:
 | 
			
		||||
        resource: '../Validator'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
<?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\DocStore;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20241212112733 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Move the title of PersonDocument and AccompanyingCourseDocument to stored object';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('UPDATE chill_doc.stored_object SET title = ac_doc.title FROM chill_doc.accompanyingcourse_document ac_doc WHERE ac_doc.object_id = stored_object.id');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document DROP scope_id');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document DROP title');
 | 
			
		||||
        $this->addSql('UPDATE chill_doc.stored_object SET title = p_doc.title FROM chill_doc.person_document p_doc WHERE p_doc.object_id = stored_object.id');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_doc.person_document DROP title');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD scope_id INT DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD title TEXT DEFAULT \'\' NOT NULL');
 | 
			
		||||
        $this->addSql('UPDATE chill_doc.accompanyingcourse_document SET title = so.title FROM chill_doc.stored_object so WHERE object_id = so.id');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD CONSTRAINT fk_a45098f6682b5931 FOREIGN KEY (scope_id) REFERENCES scopes (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('CREATE INDEX idx_a45098f6682b5931 ON chill_doc.accompanyingcourse_document (scope_id)');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_doc.person_document ADD title TEXT DEFAULT \'\' NOT NULL');
 | 
			
		||||
        $this->addSql('UPDATE chill_doc.person_document SET title = so.title FROM chill_doc.stored_object so WHERE object_id = so.id');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -86,6 +86,7 @@ workflow:
 | 
			
		||||
        shared_doc: Document partagé
 | 
			
		||||
        title: Document partagé
 | 
			
		||||
        main_document: Document principal
 | 
			
		||||
        attachment: Pièce jointe
 | 
			
		||||
 | 
			
		||||
# ROLES
 | 
			
		||||
accompanyingCourseDocument: Documents dans les parcours d'accompagnement
 | 
			
		||||
@@ -94,3 +95,7 @@ CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE: Supprimer un document
 | 
			
		||||
CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE: Voir les documents
 | 
			
		||||
CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS: Voir les détails d'un document
 | 
			
		||||
CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE: Modifier un document
 | 
			
		||||
 | 
			
		||||
entity_display_title:
 | 
			
		||||
    Document (n°%doc%): "Document (n°%doc%)"
 | 
			
		||||
    Doc for evaluation (n°%eval%): Document de l'évaluation n°%eval%
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,2 @@
 | 
			
		||||
entity_display_title:
 | 
			
		||||
    Doc for evaluation (n°%eval%): Evaluatiedocument (n°%eval%)
 | 
			
		||||
@@ -309,6 +309,7 @@ class NotificationController extends AbstractController
 | 
			
		||||
            $templateData[] = [
 | 
			
		||||
                'template' => $this->notificationHandlerManager->getTemplate($notification),
 | 
			
		||||
                'template_data' => $this->notificationHandlerManager->getTemplateData($notification),
 | 
			
		||||
                'handler' => $this->notificationHandlerManager->getHandler($notification),
 | 
			
		||||
                'notification' => $notification,
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,101 @@
 | 
			
		||||
<?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\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
 | 
			
		||||
use Chill\MainBundle\Workflow\Attachment\AddAttachmentAction;
 | 
			
		||||
use Chill\MainBundle\Workflow\Attachment\AddAttachmentRequestDTO;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
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 Symfony\Component\Validator\Validator\ValidatorInterface;
 | 
			
		||||
 | 
			
		||||
class WorkflowAttachmentController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly Security $security,
 | 
			
		||||
        private readonly SerializerInterface $serializer,
 | 
			
		||||
        private readonly ValidatorInterface $validator,
 | 
			
		||||
        private readonly EntityManagerInterface $entityManager,
 | 
			
		||||
        private readonly AddAttachmentAction $addAttachmentAction,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/main/workflow/{id}/attachment', methods: ['POST'])]
 | 
			
		||||
    public function addAttachment(EntityWorkflow $entityWorkflow, Request $request): JsonResponse
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $entityWorkflow)) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $dto = new AddAttachmentRequestDTO($entityWorkflow);
 | 
			
		||||
        $this->serializer->deserialize($request->getContent(), AddAttachmentRequestDTO::class, 'json', [
 | 
			
		||||
            AbstractNormalizer::OBJECT_TO_POPULATE => $dto, AbstractNormalizer::GROUPS => ['write'],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $errors = $this->validator->validate($dto);
 | 
			
		||||
 | 
			
		||||
        if (count($errors) > 0) {
 | 
			
		||||
            return new JsonResponse(
 | 
			
		||||
                $this->serializer->serialize($errors, 'json'),
 | 
			
		||||
                Response::HTTP_UNPROCESSABLE_ENTITY,
 | 
			
		||||
                json: true
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $attachment = ($this->addAttachmentAction)($dto);
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse(
 | 
			
		||||
            $this->serializer->serialize($attachment, 'json', [AbstractNormalizer::GROUPS => ['read']]),
 | 
			
		||||
            json: true
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/main/workflow/attachment/{id}', methods: ['DELETE'])]
 | 
			
		||||
    public function removeAttachment(EntityWorkflowAttachment $attachment): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $attachment->getEntityWorkflow())) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->remove($attachment);
 | 
			
		||||
        $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
        return new Response(null, Response::HTTP_NO_CONTENT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/main/workflow/{id}/attachment', methods: ['GET'])]
 | 
			
		||||
    public function listAttachmentsForEntityWorkflow(EntityWorkflow $entityWorkflow): JsonResponse
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $entityWorkflow)) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse(
 | 
			
		||||
            $this->serializer->serialize(
 | 
			
		||||
                $entityWorkflow->getAttachments(),
 | 
			
		||||
                'json',
 | 
			
		||||
                [AbstractNormalizer::GROUPS => ['read']]
 | 
			
		||||
            ),
 | 
			
		||||
            json: true
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -351,6 +351,7 @@ class WorkflowController extends AbstractController
 | 
			
		||||
                'entity_workflow' => $entityWorkflow,
 | 
			
		||||
                'transition_form_errors' => $errors,
 | 
			
		||||
                'signatures' => $signatures,
 | 
			
		||||
                'related_accompanying_period' => $this->entityWorkflowManager->getRelatedAccompanyingPeriod($entityWorkflow),
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -87,12 +87,19 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
 | 
			
		||||
    private string $workflowName;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<int, EntityWorkflowAttachment>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowAttachment::class, cascade: ['remove'], orphanRemoval: true)]
 | 
			
		||||
    private Collection $attachments;
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        $this->subscriberToFinal = new ArrayCollection();
 | 
			
		||||
        $this->subscriberToStep = new ArrayCollection();
 | 
			
		||||
        $this->comments = new ArrayCollection();
 | 
			
		||||
        $this->steps = new ArrayCollection();
 | 
			
		||||
        $this->attachments = new ArrayCollection();
 | 
			
		||||
 | 
			
		||||
        $initialStep = new EntityWorkflowStep();
 | 
			
		||||
        $initialStep
 | 
			
		||||
@@ -142,6 +149,35 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return $this
 | 
			
		||||
     *
 | 
			
		||||
     * @internal use @{EntityWorkflowAttachement::__construct} instead
 | 
			
		||||
     */
 | 
			
		||||
    public function addAttachment(EntityWorkflowAttachment $attachment): self
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->attachments->contains($attachment)) {
 | 
			
		||||
            $this->attachments[] = $attachment;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return Collection<int, EntityWorkflowAttachment>
 | 
			
		||||
     */
 | 
			
		||||
    public function getAttachments(): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->attachments;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function removeAttachment(EntityWorkflowAttachment $attachment): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->attachments->removeElement($attachment);
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getComments(): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->comments;
 | 
			
		||||
@@ -356,6 +392,17 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
        return $this->getCurrentStep()->isOnHoldByUser($user);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isUserInvolved(User $user): bool
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($this->getSteps() as $step) {
 | 
			
		||||
            if ($step->getAllDestUser()->contains($user)) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isUserSubscribedToFinal(User $user): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->subscriberToFinal->contains($user);
 | 
			
		||||
@@ -420,7 +467,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method use by marking store.
 | 
			
		||||
     * Method used by marking store.
 | 
			
		||||
     *
 | 
			
		||||
     * @return $this
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,80 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Doctrine\DBAL\Types\Types;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
 | 
			
		||||
#[ORM\Entity()]
 | 
			
		||||
#[ORM\Table(name: 'chill_main_workflow_entity_attachment')]
 | 
			
		||||
#[ORM\UniqueConstraint(name: 'unique_generic_doc_by_workflow', columns: ['relatedGenericDocKey', 'relatedGenericDocIdentifiers', 'entityworkflow_id'])]
 | 
			
		||||
class EntityWorkflowAttachment implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
    use TrackUpdateTrait;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\GeneratedValue]
 | 
			
		||||
    #[ORM\Column(type: Types::INTEGER)]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[ORM\Column(name: 'relatedGenericDocKey', type: Types::STRING, length: 255, nullable: false)]
 | 
			
		||||
        private string $relatedGenericDocKey,
 | 
			
		||||
        #[ORM\Column(name: 'relatedGenericDocIdentifiers', type: Types::JSON, nullable: false, options: ['jsonb' => true])]
 | 
			
		||||
        private array $relatedGenericDocIdentifiers,
 | 
			
		||||
        #[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'attachments')]
 | 
			
		||||
        #[ORM\JoinColumn(nullable: false, name: 'entityworkflow_id')]
 | 
			
		||||
        private EntityWorkflow $entityWorkflow,
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Stored object related to the generic doc.
 | 
			
		||||
         *
 | 
			
		||||
         * This is a story to keep track more easily to stored object
 | 
			
		||||
         */
 | 
			
		||||
        #[ORM\ManyToOne(targetEntity: StoredObject::class)]
 | 
			
		||||
        #[ORM\JoinColumn(nullable: false, name: 'storedobject_id')]
 | 
			
		||||
        private StoredObject $proxyStoredObject,
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->entityWorkflow->addAttachment($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getEntityWorkflow(): EntityWorkflow
 | 
			
		||||
    {
 | 
			
		||||
        return $this->entityWorkflow;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getRelatedGenericDocIdentifiers(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->relatedGenericDocIdentifiers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getRelatedGenericDocKey(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->relatedGenericDocKey;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getProxyStoredObject(): StoredObject
 | 
			
		||||
    {
 | 
			
		||||
        return $this->proxyStoredObject;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,6 +17,11 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Contains comment for entity workflow.
 | 
			
		||||
 *
 | 
			
		||||
 * **NOTE**: for now, this class is not in used. Comments are, for now, stored in the EntityWorkflowStep.
 | 
			
		||||
 */
 | 
			
		||||
#[ORM\Entity]
 | 
			
		||||
#[ORM\Table('chill_main_workflow_entity_comment')]
 | 
			
		||||
class EntityWorkflowComment implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
 
 | 
			
		||||
@@ -16,9 +16,18 @@ use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Doctrine\Common\Collections\ArrayCollection;
 | 
			
		||||
use Doctrine\Common\Collections\Collection;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A step for each EntityWorkflow.
 | 
			
		||||
 *
 | 
			
		||||
 * The step contains the history of position. The current one is the one which transitionAt or transitionAfter is NULL.
 | 
			
		||||
 *
 | 
			
		||||
 * The comments field is populated by the comment of the one who apply the transition, it means that the comment for the
 | 
			
		||||
 * "next" step is stored in the EntityWorkflowStep in the previous step.
 | 
			
		||||
 *
 | 
			
		||||
 * DestUsers are the one added at the transition. DestUserByAccessKey are the users who obtained permission after having
 | 
			
		||||
 * clicked on a link to get access (email notification to groups).
 | 
			
		||||
 */
 | 
			
		||||
#[ORM\Entity]
 | 
			
		||||
#[ORM\Table('chill_main_workflow_entity_step')]
 | 
			
		||||
class EntityWorkflowStep
 | 
			
		||||
@@ -80,6 +89,11 @@ class EntityWorkflowStep
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * If this is the final step.
 | 
			
		||||
     *
 | 
			
		||||
     * This property is filled by a listener.
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
 | 
			
		||||
    private bool $isFinal = false;
 | 
			
		||||
 | 
			
		||||
@@ -254,6 +268,11 @@ class EntityWorkflowStep
 | 
			
		||||
        return $this->ccUser;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This is the comment from the one who apply the transition.
 | 
			
		||||
     *
 | 
			
		||||
     * It means that it must be saved when the user apply a transition.
 | 
			
		||||
     */
 | 
			
		||||
    public function getComment(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->comment;
 | 
			
		||||
@@ -346,6 +365,9 @@ class EntityWorkflowStep
 | 
			
		||||
        return $this->transitionByEmail;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return bool true if this is the end of the EntityWorkflow
 | 
			
		||||
     */
 | 
			
		||||
    public function isFinal(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->isFinal;
 | 
			
		||||
@@ -367,6 +389,9 @@ class EntityWorkflowStep
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return bool if the EntityWorkflowStep is waiting for a transition, and is not the final step
 | 
			
		||||
     */
 | 
			
		||||
    public function isWaitingForTransition(): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (null !== $this->transitionAfter) {
 | 
			
		||||
@@ -506,26 +531,6 @@ class EntityWorkflowStep
 | 
			
		||||
        return $this->holdsOnStep;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Assert\Callback]
 | 
			
		||||
    public function validateOnCreation(ExecutionContextInterface $context, mixed $payload): void
 | 
			
		||||
    {
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
        if ($this->isFinalizeAfter()) {
 | 
			
		||||
            if (0 !== \count($this->getDestUser())) {
 | 
			
		||||
                $context->buildViolation('workflow.No dest users when the workflow is finalized')
 | 
			
		||||
                    ->atPath('finalizeAfter')
 | 
			
		||||
                    ->addViolation();
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            if (0 === \count($this->getDestUser())) {
 | 
			
		||||
                $context->buildViolation('workflow.The next step must count at least one dest')
 | 
			
		||||
                    ->atPath('finalizeAfter')
 | 
			
		||||
                    ->addViolation();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addOnHold(EntityWorkflowStepHold $onHold): self
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->holdsOnStep->contains($onHold)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,7 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')]
 | 
			
		||||
        #[ORM\JoinColumn(nullable: false)]
 | 
			
		||||
        private EntityWorkflowStep $step,
 | 
			
		||||
        User|Person $signer,
 | 
			
		||||
    ) {
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ class NewsItemType extends AbstractType
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('content', ChillTextareaType::class, [
 | 
			
		||||
                'required' => false,
 | 
			
		||||
                'empty_data' => '',
 | 
			
		||||
            ])
 | 
			
		||||
            ->add(
 | 
			
		||||
                'startDate',
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\MainBundle\Notification;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Notification;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatableInterface;
 | 
			
		||||
 | 
			
		||||
interface NotificationHandlerInterface
 | 
			
		||||
{
 | 
			
		||||
@@ -29,4 +30,13 @@ interface NotificationHandlerInterface
 | 
			
		||||
     * Return true if the handler supports the handling for this notification.
 | 
			
		||||
     */
 | 
			
		||||
    public function supports(Notification $notification, array $options = []): bool;
 | 
			
		||||
 | 
			
		||||
    public function getTitle(Notification $notification, array $options = []): TranslatableInterface;
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
     * return list<Person>
 | 
			
		||||
     */
 | 
			
		||||
    public function getAssociatedPersons(Notification $notification, array $options = []): array;
 | 
			
		||||
 | 
			
		||||
    public function getRelatedEntity(Notification $notification): ?object;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,11 +13,10 @@ namespace Chill\MainBundle\Notification;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Notification;
 | 
			
		||||
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class NotificationHandlerManager
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private iterable $handlers, private EntityManagerInterface $em) {}
 | 
			
		||||
    public function __construct(private iterable $handlers) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throw NotificationHandlerNotFound if handler is not found
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,67 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Repository\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @implements ObjectRepository<EntityWorkflowAttachment>
 | 
			
		||||
 */
 | 
			
		||||
class EntityWorkflowAttachmentRepository implements ObjectRepository
 | 
			
		||||
{
 | 
			
		||||
    private readonly EntityRepository $repository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(EntityManagerInterface $registry)
 | 
			
		||||
    {
 | 
			
		||||
        $this->repository = $registry->getRepository(EntityWorkflowAttachment::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function find($id): ?EntityWorkflowAttachment
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->find($id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAll(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findOneBy($criteria);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return array<EntityWorkflowAttachment>
 | 
			
		||||
     */
 | 
			
		||||
    public function findByStoredObject(StoredObject $storedObject): array
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->repository->createQueryBuilder('a');
 | 
			
		||||
        $qb->where('a.proxyStoredObject = :storedObject')->setParameter('storedObject', $storedObject);
 | 
			
		||||
 | 
			
		||||
        return $qb->getQuery()->getResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getClassName()
 | 
			
		||||
    {
 | 
			
		||||
        return EntityWorkflowAttachment::class;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -480,7 +480,7 @@ div.workflow {
 | 
			
		||||
    section.step {
 | 
			
		||||
        border: 1px solid $chill-l-gray;
 | 
			
		||||
        padding: 1em 2em;
 | 
			
		||||
        div.flex-table {
 | 
			
		||||
        > div.flex-table {
 | 
			
		||||
            margin: 1.5em -2em;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,19 @@ div.notification {
 | 
			
		||||
            margin-right: 0.3em;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    h4.notification-subtitle {
 | 
			
		||||
        margin: 0.5rem 0;
 | 
			
		||||
    }
 | 
			
		||||
    ul.notification-related-entities {
 | 
			
		||||
        margin: 0.5rem 0;
 | 
			
		||||
        list-style: none;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
 | 
			
		||||
        & > li {
 | 
			
		||||
            display: inline-block;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    div.read {
 | 
			
		||||
        h2.notification-title,
 | 
			
		||||
        h6.notification-title {
 | 
			
		||||
@@ -52,7 +65,7 @@ div.notification-show {
 | 
			
		||||
                    li {
 | 
			
		||||
                        span.item-key {
 | 
			
		||||
                            display: inline-block;
 | 
			
		||||
                            width: 3em;
 | 
			
		||||
                            padding: 0.1em 0.3rem;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@@ -97,4 +110,4 @@ span.counter {
 | 
			
		||||
        padding: 0 0.4rem;
 | 
			
		||||
        border-radius: 50%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -83,6 +83,10 @@ export const makeFetch = <Input, Output>(
 | 
			
		||||
        opts = Object.assign(opts, options);
 | 
			
		||||
    }
 | 
			
		||||
    return fetch(url, opts).then((response) => {
 | 
			
		||||
        if (response.status === 204) {
 | 
			
		||||
            return Promise.resolve();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (response.ok) {
 | 
			
		||||
            return response.json();
 | 
			
		||||
        }
 | 
			
		||||
@@ -173,18 +177,26 @@ function _fetchAction<T>(
 | 
			
		||||
 | 
			
		||||
            throw new Error("other network error");
 | 
			
		||||
        })
 | 
			
		||||
        .catch((reason: any) => {
 | 
			
		||||
            console.error(reason);
 | 
			
		||||
            throw new Error(reason);
 | 
			
		||||
        });
 | 
			
		||||
        .catch(
 | 
			
		||||
            (
 | 
			
		||||
                reason:
 | 
			
		||||
                    | NotFoundExceptionInterface
 | 
			
		||||
                    | ServerExceptionInterface
 | 
			
		||||
                    | ValidationExceptionInterface
 | 
			
		||||
                    | TransportExceptionInterface,
 | 
			
		||||
            ) => {
 | 
			
		||||
                console.error(reason);
 | 
			
		||||
                throw reason;
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const fetchResults = async <T>(
 | 
			
		||||
    uri: string,
 | 
			
		||||
    params?: FetchParams,
 | 
			
		||||
): Promise<T[]> => {
 | 
			
		||||
    let promises: Promise<T[]>[] = [],
 | 
			
		||||
        page = 1;
 | 
			
		||||
    const promises: Promise<T[]>[] = [];
 | 
			
		||||
    let page = 1;
 | 
			
		||||
    const firstData: PaginationResponse<T> = (await _fetchAction(
 | 
			
		||||
        page,
 | 
			
		||||
        uri,
 | 
			
		||||
@@ -229,6 +241,7 @@ const ValidationException = (
 | 
			
		||||
    return error;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
const AccessException = (response: Response): AccessExceptionInterface => {
 | 
			
		||||
    const error = {} as AccessExceptionInterface;
 | 
			
		||||
    error.name = "AccessException";
 | 
			
		||||
@@ -237,6 +250,7 @@ const AccessException = (response: Response): AccessExceptionInterface => {
 | 
			
		||||
    return error;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
const NotFoundException = (response: Response): NotFoundExceptionInterface => {
 | 
			
		||||
    const error = {} as NotFoundExceptionInterface;
 | 
			
		||||
    error.name = "NotFoundException";
 | 
			
		||||
@@ -257,6 +271,7 @@ const ServerException = (
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ConflictHttpException = (
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    response: Response,
 | 
			
		||||
): ConflictHttpExceptionInterface => {
 | 
			
		||||
    const error = {} as ConflictHttpExceptionInterface;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
import { WorkflowAttachment } from "ChillMainAssets/types";
 | 
			
		||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
 | 
			
		||||
 | 
			
		||||
export const find_attachments_by_workflow = async (
 | 
			
		||||
    workflowId: number,
 | 
			
		||||
): Promise<WorkflowAttachment[]> =>
 | 
			
		||||
    makeFetch("GET", `/api/1.0/main/workflow/${workflowId}/attachment`);
 | 
			
		||||
 | 
			
		||||
export const create_attachment = async (
 | 
			
		||||
    workflowId: number,
 | 
			
		||||
    genericDoc: GenericDocForAccompanyingPeriod,
 | 
			
		||||
): Promise<WorkflowAttachment> =>
 | 
			
		||||
    makeFetch("POST", `/api/1.0/main/workflow/${workflowId}/attachment`, {
 | 
			
		||||
        relatedGenericDocKey: genericDoc.key,
 | 
			
		||||
        relatedGenericDocIdentifiers: genericDoc.identifiers,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
export const delete_attachment = async (
 | 
			
		||||
    attachment: WorkflowAttachment,
 | 
			
		||||
): Promise<void> =>
 | 
			
		||||
    makeFetch("DELETE", `/api/1.0/main/workflow/attachment/${attachment.id}`);
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
 | 
			
		||||
export interface DateTime {
 | 
			
		||||
    datetime: string;
 | 
			
		||||
    datetime8601: string;
 | 
			
		||||
@@ -190,3 +192,16 @@ export interface WorkflowAvailable {
 | 
			
		||||
    name: string;
 | 
			
		||||
    text: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface WorkflowAttachment {
 | 
			
		||||
    id: number;
 | 
			
		||||
    relatedGenericDocKey: string;
 | 
			
		||||
    relatedGenericDocIdentifiers: object;
 | 
			
		||||
    createdAt: DateTime | null;
 | 
			
		||||
    createdBy: User | null;
 | 
			
		||||
    updatedAt: DateTime | null;
 | 
			
		||||
    updatedBy: User | null;
 | 
			
		||||
    genericDoc: null | GenericDoc;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type PrivateCommentEmbeddable = any;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,70 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, useTemplateRef } from "vue";
 | 
			
		||||
import type { WorkflowAttachment } from "ChillMainAssets/types";
 | 
			
		||||
import PickGenericDocModal from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue";
 | 
			
		||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
 | 
			
		||||
import { GenericDoc } from "ChillDocStoreAssets/types";
 | 
			
		||||
 | 
			
		||||
interface AppConfig {
 | 
			
		||||
    workflowId: number;
 | 
			
		||||
    accompanyingPeriodId: number;
 | 
			
		||||
    attachments: WorkflowAttachment[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
    (
 | 
			
		||||
        e: "pickGenericDoc",
 | 
			
		||||
        payload: { genericDoc: GenericDocForAccompanyingPeriod },
 | 
			
		||||
    ): void;
 | 
			
		||||
    (e: "removeAttachment", payload: { attachment: WorkflowAttachment }): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
type PickGenericModalType = InstanceType<typeof PickGenericDocModal>;
 | 
			
		||||
 | 
			
		||||
const pickDocModal = useTemplateRef<PickGenericModalType>("pickDocModal");
 | 
			
		||||
const props = defineProps<AppConfig>();
 | 
			
		||||
 | 
			
		||||
const attachedGenericDoc = computed<GenericDocForAccompanyingPeriod[]>(
 | 
			
		||||
    () =>
 | 
			
		||||
        props.attachments
 | 
			
		||||
            .map((a: WorkflowAttachment) => a.genericDoc)
 | 
			
		||||
            .filter(
 | 
			
		||||
                (g: GenericDoc | null) => g !== null,
 | 
			
		||||
            ) as GenericDocForAccompanyingPeriod[],
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const openModal = function () {
 | 
			
		||||
    pickDocModal.value?.openModal();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onPickGenericDoc = ({
 | 
			
		||||
    genericDoc,
 | 
			
		||||
}: {
 | 
			
		||||
    genericDoc: GenericDocForAccompanyingPeriod;
 | 
			
		||||
}) => {
 | 
			
		||||
    emit("pickGenericDoc", { genericDoc });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <pick-generic-doc-modal
 | 
			
		||||
        :accompanying-period-id="props.accompanyingPeriodId"
 | 
			
		||||
        :to-remove="attachedGenericDoc"
 | 
			
		||||
        ref="pickDocModal"
 | 
			
		||||
        @pickGenericDoc="onPickGenericDoc"
 | 
			
		||||
    ></pick-generic-doc-modal>
 | 
			
		||||
    <attachment-list
 | 
			
		||||
        :attachments="props.attachments"
 | 
			
		||||
        @removeAttachment="(payload) => emit('removeAttachment', payload)"
 | 
			
		||||
    ></attachment-list>
 | 
			
		||||
    <ul class="record_actions">
 | 
			
		||||
        <li>
 | 
			
		||||
            <button type="button" class="btn btn-create" @click="openModal">
 | 
			
		||||
                Ajouter une pièce jointe
 | 
			
		||||
            </button>
 | 
			
		||||
        </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { WorkflowAttachment } from "ChillMainAssets/types";
 | 
			
		||||
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
 | 
			
		||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
 | 
			
		||||
 | 
			
		||||
interface AttachmentListProps {
 | 
			
		||||
    attachments: WorkflowAttachment[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/prefer-function-type
 | 
			
		||||
    (e: "removeAttachment", payload: { attachment: WorkflowAttachment }): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const props = defineProps<AttachmentListProps>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <p
 | 
			
		||||
        v-if="props.attachments.length === 0"
 | 
			
		||||
        class="chill-no-data-statement text-center"
 | 
			
		||||
    >
 | 
			
		||||
        Aucune pièce jointe
 | 
			
		||||
    </p>
 | 
			
		||||
    <!-- TODO translate -->
 | 
			
		||||
    <div else class="flex-table">
 | 
			
		||||
        <div v-for="a in props.attachments" :key="a.id" class="item-bloc">
 | 
			
		||||
            <generic-doc-item-box
 | 
			
		||||
                v-if="a.genericDoc !== null"
 | 
			
		||||
                :generic-doc="a.genericDoc"
 | 
			
		||||
            ></generic-doc-item-box>
 | 
			
		||||
            <div class="item-row separator">
 | 
			
		||||
                <ul class="record_actions">
 | 
			
		||||
                    <li v-if="a.genericDoc?.storedObject !== null">
 | 
			
		||||
                        <document-action-buttons-group
 | 
			
		||||
                            :stored-object="a.genericDoc.storedObject"
 | 
			
		||||
                        ></document-action-buttons-group>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            class="btn btn-delete"
 | 
			
		||||
                            @click="emit('removeAttachment', { attachment: a })"
 | 
			
		||||
                        ></button>
 | 
			
		||||
                    </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
 | 
			
		||||
interface GenericDocItemBoxProps {
 | 
			
		||||
    genericDoc: GenericDocForAccompanyingPeriod;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<GenericDocItemBoxProps>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="'html' in props.genericDoc.metadata"
 | 
			
		||||
        v-html="props.genericDoc.metadata.html"
 | 
			
		||||
    ></div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,270 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import {
 | 
			
		||||
    GenericDoc,
 | 
			
		||||
    GenericDocForAccompanyingPeriod,
 | 
			
		||||
} from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
import PickGenericDocItem from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue";
 | 
			
		||||
import { fetch_generic_docs_by_accompanying_period } from "ChillDocStoreAssets/js/generic-doc-api";
 | 
			
		||||
import { computed, onMounted, ref } from "vue";
 | 
			
		||||
 | 
			
		||||
interface PickGenericDocProps {
 | 
			
		||||
    accompanyingPeriodId: number;
 | 
			
		||||
    pickedList: GenericDocForAccompanyingPeriod[];
 | 
			
		||||
    toRemove: GenericDocForAccompanyingPeriod[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<PickGenericDocProps>();
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
    (
 | 
			
		||||
        e: "pickGenericDoc",
 | 
			
		||||
        payload: { genericDoc: GenericDocForAccompanyingPeriod },
 | 
			
		||||
    ): void;
 | 
			
		||||
 | 
			
		||||
    (
 | 
			
		||||
        e: "removeGenericDoc",
 | 
			
		||||
        payload: { genericDoc: GenericDocForAccompanyingPeriod },
 | 
			
		||||
    ): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const genericDocs = ref<GenericDocForAccompanyingPeriod[]>([]);
 | 
			
		||||
const loaded = ref(false);
 | 
			
		||||
 | 
			
		||||
const isPicked = (genericDoc: GenericDocForAccompanyingPeriod): boolean =>
 | 
			
		||||
    props.pickedList.findIndex(
 | 
			
		||||
        (element: GenericDocForAccompanyingPeriod) =>
 | 
			
		||||
            element.uniqueKey === genericDoc.uniqueKey,
 | 
			
		||||
    ) !== -1;
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    genericDocs.value = await fetch_generic_docs_by_accompanying_period(
 | 
			
		||||
        props.accompanyingPeriodId,
 | 
			
		||||
    );
 | 
			
		||||
    loaded.value = true;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const textFilter = ref<string>("");
 | 
			
		||||
const dateFromFilter = ref<string | null>(null);
 | 
			
		||||
const dateToFilter = ref<string | null>(null);
 | 
			
		||||
const placesFilter = ref<string[]>([]);
 | 
			
		||||
 | 
			
		||||
const availablePlaces = computed<string[]>(() => {
 | 
			
		||||
    const places = new Set<string>(
 | 
			
		||||
        genericDocs.value.map((genericDoc: GenericDoc) => genericDoc.key),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return Array.from(places).sort((a, b) => (a < b ? -1 : a === b ? 0 : 1));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const placeTrans = (str: string): string => {
 | 
			
		||||
    switch (str) {
 | 
			
		||||
        case "accompanying_course_document":
 | 
			
		||||
            return "Documents du parcours";
 | 
			
		||||
        case "person_document":
 | 
			
		||||
            return "Documents de l'usager";
 | 
			
		||||
        case "accompanying_period_calendar_document":
 | 
			
		||||
            return "Document des rendez-vous des parcours";
 | 
			
		||||
        case "accompanying_period_activity_document":
 | 
			
		||||
            return "Document des échanges des parcours";
 | 
			
		||||
        case "accompanying_period_work_evaluation_document":
 | 
			
		||||
            return "Document des actions d'accompagnement";
 | 
			
		||||
        default:
 | 
			
		||||
            return str;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filteredDocuments = computed<GenericDocForAccompanyingPeriod[]>(() => {
 | 
			
		||||
    if (false === loaded.value) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return genericDocs.value
 | 
			
		||||
        .filter(
 | 
			
		||||
            (genericDoc: GenericDocForAccompanyingPeriod) =>
 | 
			
		||||
                !props.toRemove
 | 
			
		||||
                    .map((g: GenericDocForAccompanyingPeriod) => g.uniqueKey)
 | 
			
		||||
                    .includes(genericDoc.uniqueKey),
 | 
			
		||||
        )
 | 
			
		||||
        .filter((genericDoc: GenericDocForAccompanyingPeriod) => {
 | 
			
		||||
            if (textFilter.value === "") {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const needles = textFilter.value
 | 
			
		||||
                .trim()
 | 
			
		||||
                .split(" ")
 | 
			
		||||
                .map((str: string) => str.trim().toLowerCase())
 | 
			
		||||
                .filter((str: string) => str.length > 0);
 | 
			
		||||
            const title: string =
 | 
			
		||||
                "title" in genericDoc.metadata
 | 
			
		||||
                    ? (genericDoc.metadata.title as string)
 | 
			
		||||
                    : "";
 | 
			
		||||
            if (title === "") {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return needles.every((n: string) =>
 | 
			
		||||
                title.toLowerCase().includes(n),
 | 
			
		||||
            );
 | 
			
		||||
        })
 | 
			
		||||
        .filter((genericDoc: GenericDocForAccompanyingPeriod) => {
 | 
			
		||||
            if (placesFilter.value.length === 0) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return placesFilter.value.includes(genericDoc.key);
 | 
			
		||||
        })
 | 
			
		||||
        .filter((genericDoc: GenericDocForAccompanyingPeriod) => {
 | 
			
		||||
            if (dateToFilter.value === null) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return genericDoc.doc_date.datetime8601 < dateToFilter.value;
 | 
			
		||||
        })
 | 
			
		||||
        .filter((genericDoc: GenericDocForAccompanyingPeriod) => {
 | 
			
		||||
            if (dateFromFilter.value === null) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return genericDoc.doc_date.datetime8601 > dateFromFilter.value;
 | 
			
		||||
        });
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <div v-if="loaded">
 | 
			
		||||
        <div>
 | 
			
		||||
            <form name="f" method="get">
 | 
			
		||||
                <div class="accordion my-3" id="filterOrderAccordion">
 | 
			
		||||
                    <h2 class="accordion-header" id="filterOrderHeading">
 | 
			
		||||
                        <button
 | 
			
		||||
                            class="accordion-button"
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            data-bs-toggle="collapse"
 | 
			
		||||
                            data-bs-target="#filterOrderCollapse"
 | 
			
		||||
                            aria-expanded="true"
 | 
			
		||||
                            aria-controls="filterOrderCollapse"
 | 
			
		||||
                        >
 | 
			
		||||
                            <strong
 | 
			
		||||
                                ><i class="fa fa-fw fa-filter"></i>Filtrer la
 | 
			
		||||
                                liste</strong
 | 
			
		||||
                            >
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </h2>
 | 
			
		||||
                    <div
 | 
			
		||||
                        class="accordion-collapse collapse"
 | 
			
		||||
                        id="filterOrderCollapse"
 | 
			
		||||
                        aria-labelledby="filterOrderHeading"
 | 
			
		||||
                        data-bs-parent="#filterOrderAccordion"
 | 
			
		||||
                        style=""
 | 
			
		||||
                    >
 | 
			
		||||
                        <div
 | 
			
		||||
                            class="accordion-body chill_filter_order container-xxl p-5 py-2"
 | 
			
		||||
                        >
 | 
			
		||||
                            <div class="row my-2">
 | 
			
		||||
                                <div class="col-sm-12">
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input
 | 
			
		||||
                                            v-model="textFilter"
 | 
			
		||||
                                            type="search"
 | 
			
		||||
                                            id="f_q"
 | 
			
		||||
                                            name="f[q]"
 | 
			
		||||
                                            placeholder="Chercher dans la liste"
 | 
			
		||||
                                            class="form-control"
 | 
			
		||||
                                        />
 | 
			
		||||
                                        <button
 | 
			
		||||
                                            type="submit"
 | 
			
		||||
                                            class="btn btn-misc"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            <i class="fa fa-search"></i>
 | 
			
		||||
                                        </button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <div class="row my-2">
 | 
			
		||||
                                <legend
 | 
			
		||||
                                    class="col-form-label col-sm-4 required"
 | 
			
		||||
                                >
 | 
			
		||||
                                    Date du document
 | 
			
		||||
                                </legend>
 | 
			
		||||
                                <div class="col-sm-8 pt-1">
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <span class="input-group-text">Du</span>
 | 
			
		||||
                                        <input
 | 
			
		||||
                                            v-model="dateFromFilter"
 | 
			
		||||
                                            type="date"
 | 
			
		||||
                                            id="f_dateRanges_dateRange_from"
 | 
			
		||||
                                            name="f[dateRanges][dateRange][from]"
 | 
			
		||||
                                            class="form-control"
 | 
			
		||||
                                        />
 | 
			
		||||
                                        <span class="input-group-text">Au</span>
 | 
			
		||||
                                        <input
 | 
			
		||||
                                            v-model="dateToFilter"
 | 
			
		||||
                                            type="date"
 | 
			
		||||
                                            id="f_dateRanges_dateRange_to"
 | 
			
		||||
                                            name="f[dateRanges][dateRange][to]"
 | 
			
		||||
                                            class="form-control"
 | 
			
		||||
                                        />
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <div class="row my-2">
 | 
			
		||||
                                <div class="col-sm-4 col-form-label">
 | 
			
		||||
                                    Filtrer par
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="col-sm-8 pt-2">
 | 
			
		||||
                                    <div
 | 
			
		||||
                                        class="form-check"
 | 
			
		||||
                                        v-for="p in availablePlaces"
 | 
			
		||||
                                        :key="p"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <input
 | 
			
		||||
                                            type="checkbox"
 | 
			
		||||
                                            v-model="placesFilter"
 | 
			
		||||
                                            name="f[checkboxes][places][]"
 | 
			
		||||
                                            class="form-check-input"
 | 
			
		||||
                                            :value="p"
 | 
			
		||||
                                        />
 | 
			
		||||
                                        <label class="form-check-label">{{
 | 
			
		||||
                                            placeTrans(p)
 | 
			
		||||
                                        }}</label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div v-if="genericDocs.length > 0" class="flex-table chill-task-list">
 | 
			
		||||
            <pick-generic-doc-item
 | 
			
		||||
                v-for="g in filteredDocuments"
 | 
			
		||||
                :key="g.uniqueKey"
 | 
			
		||||
                :accompanying-period-id="accompanyingPeriodId"
 | 
			
		||||
                :genericDoc="g"
 | 
			
		||||
                :is-picked="isPicked(g)"
 | 
			
		||||
                @pickGenericDoc="(payload) => emit('pickGenericDoc', payload)"
 | 
			
		||||
                @removeGenericDoc="
 | 
			
		||||
                    (payload) => emit('removeGenericDoc', payload)
 | 
			
		||||
                "
 | 
			
		||||
            ></pick-generic-doc-item>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else class="text-center chill-no-data-statement">
 | 
			
		||||
            Aucun document dans ce parcours
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div v-else>
 | 
			
		||||
        <div class="d-flex align-items-center">
 | 
			
		||||
            <strong>Chargement…</strong>
 | 
			
		||||
            <div
 | 
			
		||||
                class="spinner-border ms-auto"
 | 
			
		||||
                role="status"
 | 
			
		||||
                aria-hidden="true"
 | 
			
		||||
            ></div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,95 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
 | 
			
		||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
 | 
			
		||||
 | 
			
		||||
interface PickGenericDocItemProps {
 | 
			
		||||
    genericDoc: GenericDocForAccompanyingPeriod;
 | 
			
		||||
    accompanyingPeriodId: number;
 | 
			
		||||
    isPicked: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<PickGenericDocItemProps>();
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
    (
 | 
			
		||||
        e: "pickGenericDoc",
 | 
			
		||||
        payload: { genericDoc: GenericDocForAccompanyingPeriod },
 | 
			
		||||
    ): void;
 | 
			
		||||
 | 
			
		||||
    (
 | 
			
		||||
        e: "removeGenericDoc",
 | 
			
		||||
        payload: { genericDoc: GenericDocForAccompanyingPeriod },
 | 
			
		||||
    ): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const clickOnAddButton = () => {
 | 
			
		||||
    emit("pickGenericDoc", { genericDoc: props.genericDoc });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="item-bloc" :class="{ isPicked: isPicked }">
 | 
			
		||||
        <generic-doc-item-box
 | 
			
		||||
            :generic-doc="props.genericDoc"
 | 
			
		||||
        ></generic-doc-item-box>
 | 
			
		||||
 | 
			
		||||
        <div class="item-row separator">
 | 
			
		||||
            <ul class="record_actions">
 | 
			
		||||
                <li v-if="props.genericDoc.storedObject !== null">
 | 
			
		||||
                    <document-action-buttons-group
 | 
			
		||||
                        :stored-object="props.genericDoc.storedObject"
 | 
			
		||||
                    ></document-action-buttons-group>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li>
 | 
			
		||||
                    <button
 | 
			
		||||
                        v-if="!isPicked"
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        class="btn btn-chill-green text-white"
 | 
			
		||||
                        @click="clickOnAddButton"
 | 
			
		||||
                    >
 | 
			
		||||
                        <i class="bi bi-cart-plus"></i>
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <button
 | 
			
		||||
                        v-else
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        class="btn btn-chill-red text-white"
 | 
			
		||||
                        @click="
 | 
			
		||||
                            emit('removeGenericDoc', {
 | 
			
		||||
                                genericDoc: props.genericDoc,
 | 
			
		||||
                            })
 | 
			
		||||
                        "
 | 
			
		||||
                    >
 | 
			
		||||
                        <i class="bi bi-cart-dash"></i>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.item-bloc {
 | 
			
		||||
    &.isPicked {
 | 
			
		||||
        background: linear-gradient(
 | 
			
		||||
                180deg,
 | 
			
		||||
                rgba(25, 135, 84, 1) 0px,
 | 
			
		||||
                rgba(25, 135, 84, 0) 9px
 | 
			
		||||
            ),
 | 
			
		||||
            linear-gradient(
 | 
			
		||||
                270deg,
 | 
			
		||||
                rgba(25, 135, 84, 1) 0px,
 | 
			
		||||
                rgba(25, 135, 84, 0) 9px
 | 
			
		||||
            ),
 | 
			
		||||
            linear-gradient(
 | 
			
		||||
                0deg,
 | 
			
		||||
                rgba(25, 135, 84, 1) 0px,
 | 
			
		||||
                rgba(25, 135, 84, 0) 9px
 | 
			
		||||
            ),
 | 
			
		||||
            linear-gradient(
 | 
			
		||||
                90deg,
 | 
			
		||||
                rgba(25, 135, 84, 1) 0px,
 | 
			
		||||
                rgba(25, 135, 84, 0) 9px
 | 
			
		||||
            );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,113 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
 | 
			
		||||
import { computed, ref, useTemplateRef } from "vue";
 | 
			
		||||
import PickGenericDoc from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue";
 | 
			
		||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
 | 
			
		||||
interface PickGenericDocModalProps {
 | 
			
		||||
    accompanyingPeriodId: number;
 | 
			
		||||
    toRemove: GenericDocForAccompanyingPeriod[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PickGenericDocType = InstanceType<typeof PickGenericDoc>;
 | 
			
		||||
 | 
			
		||||
const props = defineProps<PickGenericDocModalProps>();
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/prefer-function-type
 | 
			
		||||
    (
 | 
			
		||||
        e: "pickGenericDoc",
 | 
			
		||||
        payload: { genericDoc: GenericDocForAccompanyingPeriod },
 | 
			
		||||
    ): void;
 | 
			
		||||
}>();
 | 
			
		||||
const picker = useTemplateRef<PickGenericDocType>("picker");
 | 
			
		||||
const modalOpened = ref<boolean>(false);
 | 
			
		||||
const pickeds = ref<GenericDocForAccompanyingPeriod[]>([]);
 | 
			
		||||
const modalClasses = { "modal-xl": true, "modal-dialog-scrollable": true };
 | 
			
		||||
 | 
			
		||||
const numberOfPicked = computed<number>(() => pickeds.value.length);
 | 
			
		||||
 | 
			
		||||
const onPicked = ({
 | 
			
		||||
    genericDoc,
 | 
			
		||||
}: {
 | 
			
		||||
    genericDoc: GenericDocForAccompanyingPeriod;
 | 
			
		||||
}) => {
 | 
			
		||||
    pickeds.value.push(genericDoc);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onRemove = ({
 | 
			
		||||
    genericDoc,
 | 
			
		||||
}: {
 | 
			
		||||
    genericDoc: GenericDocForAccompanyingPeriod;
 | 
			
		||||
}) => {
 | 
			
		||||
    const index = pickeds.value.findIndex(
 | 
			
		||||
        (item) => item.uniqueKey === genericDoc.uniqueKey,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (index === -1) {
 | 
			
		||||
        throw new Error("Remove generic doc that doesn't exist");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pickeds.value.splice(index, 1);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onConfirm = () => {
 | 
			
		||||
    for (let genericDoc of pickeds.value) {
 | 
			
		||||
        emit("pickGenericDoc", { genericDoc });
 | 
			
		||||
    }
 | 
			
		||||
    pickeds.value = [];
 | 
			
		||||
    closeModal();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeModal = function () {
 | 
			
		||||
    modalOpened.value = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const openModal = function () {
 | 
			
		||||
    modalOpened.value = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ openModal, closeModal });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <modal
 | 
			
		||||
        v-if="modalOpened"
 | 
			
		||||
        @close="closeModal"
 | 
			
		||||
        :modal-dialog-class="modalClasses"
 | 
			
		||||
    >
 | 
			
		||||
        <template v-slot:header>
 | 
			
		||||
            <h2 class="modal-title">Ajouter une pièce jointe</h2>
 | 
			
		||||
        </template>
 | 
			
		||||
        <template v-slot:body>
 | 
			
		||||
            <pick-generic-doc
 | 
			
		||||
                :accompanying-period-id="props.accompanyingPeriodId"
 | 
			
		||||
                :to-remove="props.toRemove"
 | 
			
		||||
                :picked-list="pickeds"
 | 
			
		||||
                ref="picker"
 | 
			
		||||
                @pickGenericDoc="onPicked"
 | 
			
		||||
                @removeGenericDoc="onRemove"
 | 
			
		||||
            ></pick-generic-doc>
 | 
			
		||||
        </template>
 | 
			
		||||
        <template v-slot:footer>
 | 
			
		||||
            <ul v-if="numberOfPicked > 0" class="record_actions">
 | 
			
		||||
                <li>
 | 
			
		||||
                    <button
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        class="btn btn-chill-green text-white"
 | 
			
		||||
                        @click="onConfirm"
 | 
			
		||||
                    >
 | 
			
		||||
                        <template v-if="numberOfPicked > 1">
 | 
			
		||||
                            <i class="fa fa-plus"></i> Ajouter
 | 
			
		||||
                            {{ numberOfPicked }} pièces jointes
 | 
			
		||||
                        </template>
 | 
			
		||||
                        <template v-else>
 | 
			
		||||
                            <i class="fa fa-plus"></i> Ajouter une pièce jointe
 | 
			
		||||
                        </template>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </template>
 | 
			
		||||
    </modal>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,109 @@
 | 
			
		||||
import { createApp } from "vue";
 | 
			
		||||
import App from "./App.vue";
 | 
			
		||||
import { _createI18n } from "../_js/i18n";
 | 
			
		||||
import { WorkflowAttachment } from "ChillMainAssets/types";
 | 
			
		||||
import {
 | 
			
		||||
    create_attachment,
 | 
			
		||||
    delete_attachment,
 | 
			
		||||
    find_attachments_by_workflow,
 | 
			
		||||
} from "ChillMainAssets/lib/workflow/attachments";
 | 
			
		||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
import ToastPlugin from "vue-toast-notification";
 | 
			
		||||
import "vue-toast-notification/dist/theme-bootstrap.css";
 | 
			
		||||
 | 
			
		||||
window.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
    const attachments = document.querySelectorAll<HTMLDivElement>(
 | 
			
		||||
        'div[data-app="workflow_attachments"]',
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    attachments.forEach(async (el) => {
 | 
			
		||||
        const workflowId = parseInt(el.dataset.entityWorkflowId || "");
 | 
			
		||||
        const accompanyingPeriodId = parseInt(
 | 
			
		||||
            el.dataset.relatedAccompanyingPeriodId || "",
 | 
			
		||||
        );
 | 
			
		||||
        const attachments = await find_attachments_by_workflow(workflowId);
 | 
			
		||||
 | 
			
		||||
        const app = createApp({
 | 
			
		||||
            template:
 | 
			
		||||
                '<app :workflowId="workflowId" :accompanyingPeriodId="accompanyingPeriodId" :attachments="attachments" @pickGenericDoc="onPickGenericDoc" @removeAttachment="onRemoveAttachment"></app>',
 | 
			
		||||
            components: { App },
 | 
			
		||||
            data: function () {
 | 
			
		||||
                return { workflowId, accompanyingPeriodId, attachments };
 | 
			
		||||
            },
 | 
			
		||||
            methods: {
 | 
			
		||||
                onRemoveAttachment: async function ({
 | 
			
		||||
                    attachment,
 | 
			
		||||
                }: {
 | 
			
		||||
                    attachment: WorkflowAttachment;
 | 
			
		||||
                }): Promise<void> {
 | 
			
		||||
                    const index = this.$data.attachments.findIndex(
 | 
			
		||||
                        (el: WorkflowAttachment) => el.id === attachment.id,
 | 
			
		||||
                    );
 | 
			
		||||
                    if (-1 === index) {
 | 
			
		||||
                        console.warn(
 | 
			
		||||
                            "this attachment is not associated with the workflow",
 | 
			
		||||
                            attachment,
 | 
			
		||||
                        );
 | 
			
		||||
                        this.$toast.error(
 | 
			
		||||
                            "This attachment is not associated with the workflow",
 | 
			
		||||
                        );
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    try {
 | 
			
		||||
                        await delete_attachment(attachment);
 | 
			
		||||
                    } catch (error) {
 | 
			
		||||
                        console.error(error);
 | 
			
		||||
                        this.$toast.error("Error while removing element");
 | 
			
		||||
                        throw error;
 | 
			
		||||
                    }
 | 
			
		||||
                    this.$data.attachments.splice(index, 1);
 | 
			
		||||
                    this.$toast.success("Pièce jointe supprimée");
 | 
			
		||||
                },
 | 
			
		||||
                onPickGenericDoc: async function ({
 | 
			
		||||
                    genericDoc,
 | 
			
		||||
                }: {
 | 
			
		||||
                    genericDoc: GenericDocForAccompanyingPeriod;
 | 
			
		||||
                }): Promise<void> {
 | 
			
		||||
                    console.log("picked generic doc", genericDoc);
 | 
			
		||||
 | 
			
		||||
                    // prevent to create double attachment:
 | 
			
		||||
                    if (
 | 
			
		||||
                        -1 !==
 | 
			
		||||
                        this.$data.attachments.findIndex(
 | 
			
		||||
                            (el: WorkflowAttachment) =>
 | 
			
		||||
                                el.genericDoc?.key === genericDoc.key &&
 | 
			
		||||
                                JSON.stringify(el.genericDoc?.identifiers) ==
 | 
			
		||||
                                    JSON.stringify(genericDoc.identifiers),
 | 
			
		||||
                        )
 | 
			
		||||
                    ) {
 | 
			
		||||
                        console.warn(
 | 
			
		||||
                            "this document is already attached to the workflow",
 | 
			
		||||
                            genericDoc,
 | 
			
		||||
                        );
 | 
			
		||||
                        this.$toast.error(
 | 
			
		||||
                            "Ce document est déjà attaché au workflow",
 | 
			
		||||
                        );
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    try {
 | 
			
		||||
                        const attachment = await create_attachment(
 | 
			
		||||
                            workflowId,
 | 
			
		||||
                            genericDoc,
 | 
			
		||||
                        );
 | 
			
		||||
                        this.$data.attachments.push(attachment);
 | 
			
		||||
                    } catch (error) {
 | 
			
		||||
                        console.error(error);
 | 
			
		||||
                        throw error;
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
        const i18n = _createI18n({});
 | 
			
		||||
        app.use(i18n);
 | 
			
		||||
        app.use(ToastPlugin);
 | 
			
		||||
 | 
			
		||||
        app.mount(el);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -11,21 +11,59 @@
 | 
			
		||||
        </h2>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endmacro %}
 | 
			
		||||
 | 
			
		||||
{% macro insert_onthefly(type, entity, parent = null) %}
 | 
			
		||||
    {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
 | 
			
		||||
        action: 'show', displayBadge: true,
 | 
			
		||||
        targetEntity: { name: type, id: entity.id },
 | 
			
		||||
        buttonText: entity|chill_entity_render_string,
 | 
			
		||||
        isDead: entity.deathdate is defined and entity.deathdate is not null,
 | 
			
		||||
        parent: parent
 | 
			
		||||
    } %}
 | 
			
		||||
{% endmacro %}
 | 
			
		||||
 | 
			
		||||
{% macro relatedEntity(c) %}
 | 
			
		||||
    {% if c.data is defined %}
 | 
			
		||||
        {% set notification = c.data.notification %}
 | 
			
		||||
        {% set handler = c.data.handler %}
 | 
			
		||||
        <div class="item-row">
 | 
			
		||||
            <h4 class="notification-subtitle">
 | 
			
		||||
                <span>{{ handler.getTitle(notification)|trans }}</span>
 | 
			
		||||
            </h4>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% set associateds = handler.getAssociatedPersons(notification) %}
 | 
			
		||||
        {% if associateds|length > 0 %}
 | 
			
		||||
            <div class="item-row">
 | 
			
		||||
                <ul class="notification-related-entities">
 | 
			
		||||
                    {% for ap in associateds %}
 | 
			
		||||
                        <li>
 | 
			
		||||
                            {% if ap.person is defined %}
 | 
			
		||||
                                {{ _self.insert_onthefly('person', ap.person) }}
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                                {{ _self.insert_onthefly('person', ap) }}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </li>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
{% endmacro %}
 | 
			
		||||
{% macro header(c) %}
 | 
			
		||||
    <div class="item-row notification-header mt-2">
 | 
			
		||||
        <div class="item-col">
 | 
			
		||||
            <ul class="small_in_title">
 | 
			
		||||
                {% if c.step is not defined or c.step == 'inbox' %}
 | 
			
		||||
                    <li class="notification-from">
 | 
			
		||||
                <span class="item-key">
 | 
			
		||||
                    <abbr title="{{ 'notification.received_from' | trans }}">
 | 
			
		||||
                        {{ "notification.from" | trans }} :
 | 
			
		||||
                    </abbr>
 | 
			
		||||
                </span>
 | 
			
		||||
                        <span class="item-key">
 | 
			
		||||
                            <abbr title="{{ 'notification.received_from' | trans }}">
 | 
			
		||||
                                {{ "notification.from" | trans }} :
 | 
			
		||||
                            </abbr>
 | 
			
		||||
                        </span>
 | 
			
		||||
                        {% if not c.notification.isSystem %}
 | 
			
		||||
                            <span class="badge-user">
 | 
			
		||||
                    {{ c.notification.sender | chill_entity_render_string({'at_date': c.notification.date}) }}
 | 
			
		||||
                </span>
 | 
			
		||||
                                {{ c.notification.sender | chill_entity_render_string({'at_date': c.notification.date}) }}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <span class="badge-user system">{{ "notification.is_system" | trans }}</span>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
@@ -185,6 +223,7 @@
 | 
			
		||||
            >
 | 
			
		||||
                {{ _self.title(_context) }}
 | 
			
		||||
            </button>
 | 
			
		||||
            {{ _self.relatedEntity(_context) }}
 | 
			
		||||
            {{ _self.header(_context) }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div
 | 
			
		||||
@@ -198,6 +237,7 @@
 | 
			
		||||
        {{ _self.actions(_context) }}
 | 
			
		||||
    {% else %}
 | 
			
		||||
        {{ _self.title(_context) }}
 | 
			
		||||
        {{ _self.relatedEntity(_context) }}
 | 
			
		||||
        {{ _self.header(_context) }}
 | 
			
		||||
        {{ _self.content(_context) }}
 | 
			
		||||
        {{ _self.actions(_context) }}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,9 @@
 | 
			
		||||
        {% include '@ChillMain/Notification/_list_item.html.twig' with {
 | 
			
		||||
            'data': {
 | 
			
		||||
                 'template': handler.getTemplate(notification),
 | 
			
		||||
                 'template_data': handler.getTemplateData(notification)
 | 
			
		||||
                 'template_data': handler.getTemplateData(notification),
 | 
			
		||||
                 'handler': handler,
 | 
			
		||||
                 'notification': notification
 | 
			
		||||
            },
 | 
			
		||||
            'action_button': false,
 | 
			
		||||
            'full_content': true,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,123 +1,5 @@
 | 
			
		||||
{# TODO
 | 
			
		||||
    Check if this template is used
 | 
			
		||||
    Adapt condition or Delete it
 | 
			
		||||
#}
 | 
			
		||||
{% if random(1) == 0 %}
 | 
			
		||||
 | 
			
		||||
    {# For a document #}
 | 
			
		||||
    <h2>{{ 'Document'|trans ~ 'target'|trans }}</h2>
 | 
			
		||||
 | 
			
		||||
    <div class="row justify-content-center mt-5">
 | 
			
		||||
        <div class="col-2">
 | 
			
		||||
            <i class="fa fa-4x fa-file-text-o text-success"></i>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-8">
 | 
			
		||||
            <h3>Imprimé unique, parcours n°14635</h3>
 | 
			
		||||
            <small>Document PDF (6.2 Mo)</small>
 | 
			
		||||
            <p class="mt-2">
 | 
			
		||||
                Description du document. Sed euismod nisi porta lorem mollis aliquam. Non curabitur gravida arcu ac tortor.
 | 
			
		||||
            </p>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
{% else %}
 | 
			
		||||
 | 
			
		||||
    {# For an action #}
 | 
			
		||||
    <h2>{{ 'Accompanying Course Action'|trans ~ 'target'|trans }}</h2>
 | 
			
		||||
 | 
			
		||||
    <div class="flex-table accompanying-course-work">
 | 
			
		||||
        {# dynamic insertion
 | 
			
		||||
            ::: TODO delete all static insertion, remove condition and pass work object in inclusion
 | 
			
		||||
        #}{% if dynamic is defined %}
 | 
			
		||||
 | 
			
		||||
            {% set work = '<pass work object here>' %}
 | 
			
		||||
            {% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with { 'w': work } %}
 | 
			
		||||
 | 
			
		||||
        {% else %}
 | 
			
		||||
 | 
			
		||||
        {# BEGIN static insertion #}
 | 
			
		||||
            <div class="item-bloc">
 | 
			
		||||
                <div class="item-row">
 | 
			
		||||
                    <h2 class="badge-title">
 | 
			
		||||
                        <span class="title_label"></span>
 | 
			
		||||
                        <span class="title_action">Exercer un AEB > Conclure l'AEB
 | 
			
		||||
                            <ul class="small_in_title columns mt-1">
 | 
			
		||||
                                <li><span class="item-key">Date de début : </span><b>25/11/2021</b></li>
 | 
			
		||||
                                <li><span class="item-key">Date de fin : </span><b>10/03/2022</b></li>
 | 
			
		||||
                            </ul>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </h2>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="item-row separator">
 | 
			
		||||
                    <div class="wrap-list">
 | 
			
		||||
                        <div class="wl-row">
 | 
			
		||||
                            <div class="wl-col title"><h3>Référent</h3></div>
 | 
			
		||||
                            <div class="wl-col list"><p class="wl-item">Fred</p></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="wl-row">
 | 
			
		||||
                            <div class="wl-col title"><h3>Usagers du parcours</h3></div>
 | 
			
		||||
                            <div class="wl-col list"><span class="wl-item">
 | 
			
		||||
                                <span class="onthefly-container" data-target-name="person" data-target-id="1937" data-action="show" data-button-text="Vernon SUBUTEX" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span class="chill-entity entity-person badge-person" data-v-0c1a1125="">Vernon SUBUTEX</span></a><!--teleport start--><!--teleport end--></span></span>
 | 
			
		||||
                                <span class="wl-item"><span class="onthefly-container" data-target-name="person" data-target-id="1941" data-action="show" data-button-text="Juan RAMON" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span class="chill-entity entity-person badge-person" data-v-0c1a1125="">Juan RAMON</span></a><!--teleport start--><!--teleport end--></span></span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="wl-row">
 | 
			
		||||
                            <div class="wl-col title"><h3>Problématique sociale</h3></div>
 | 
			
		||||
                            <div class="wl-col list">
 | 
			
		||||
                                <p class="wl-item social-issues">
 | 
			
		||||
                                    <span class="chill-entity entity-social-issue"><span class="badge bg-chill-l-gray text-dark"><span class="parent-0">AD - PREVENTION, ACCES AUX DROITS, BUDGET ></span><span class="child">SOUTIEN EQUILIBRE BUDGET</span></span></span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="item-row column">
 | 
			
		||||
                    <table class="obj-res-eval smallfont my-3">
 | 
			
		||||
                        <thead>
 | 
			
		||||
                        <tr><th class="obj"><h4 class="title_label">Objectif - motif - dispositif</h4></th>
 | 
			
		||||
                            <th class="res"><h4 class="title_label">Résultats - orientations</h4></th>
 | 
			
		||||
                        </tr></thead>
 | 
			
		||||
                        <tbody>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td class="obj">
 | 
			
		||||
                                <p class="chill-no-data-statement">Aucun objectif - motif -  dispositif</p>
 | 
			
		||||
                            </td>
 | 
			
		||||
                            <td class="res">
 | 
			
		||||
                                <ul class="result_list">
 | 
			
		||||
                                    <li>Résultat : Arrêt à l'initiative du ménage pour déménagement</li>
 | 
			
		||||
                                    <li>Orientation vers une MASP</li>
 | 
			
		||||
                                </ul>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        </tbody>
 | 
			
		||||
                    </table>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="item-row separator">
 | 
			
		||||
                    <div class="updatedBy">
 | 
			
		||||
                        Dernière mise à jour par
 | 
			
		||||
                        <b><span class="chill-entity entity-user">Fred<span class="user-job">(Responsable tous les territoires)</span><span class="main-scope">(ASE)</span></span></b>,<br>
 | 
			
		||||
                        le 3 décembre 2021 à 15:19
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        {# END static insertion #}
 | 
			
		||||
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
{% if related_accompanying_period is not null %}
 | 
			
		||||
    <h2>{{ 'workflow.attachments.title'|trans }}</h2>
 | 
			
		||||
 | 
			
		||||
    <div data-app="workflow_attachments" data-workflow-id="{{ entity_workflow.id }}" data-related-accompanying-period-id="{{ related_accompanying_period.id }}" data-entity-workflow-id="{{ entity_workflow.id }}" ></div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
<ul class="record_actions">
 | 
			
		||||
    <li>
 | 
			
		||||
        <button type="button" class="btn btn-misc">
 | 
			
		||||
            <i class="fa fa-download fa-fw"></i>{{ 'Download'|trans }}
 | 
			
		||||
        </button>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li>
 | 
			
		||||
        {% set x = random(1) %}
 | 
			
		||||
        <button class="btn btn-update change-icon {% if x == 1 %}disabled{% endif %}">
 | 
			
		||||
            <i class="fa fa-fw fa-{% if x == 0 %}un{% endif %}lock"></i>
 | 
			
		||||
            {{ 'Edit'|trans }}
 | 
			
		||||
        </button>
 | 
			
		||||
    </li>
 | 
			
		||||
</ul>
 | 
			
		||||
 
 | 
			
		||||
@@ -11,5 +11,5 @@
 | 
			
		||||
        }) %}
 | 
			
		||||
    </div>
 | 
			
		||||
{% else %}
 | 
			
		||||
    <h2>{{ 'workflow.deleted_title'|trans }}</h2>
 | 
			
		||||
    <h2>{{ 'workflow.deleted'|trans }}</h2>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@
 | 
			
		||||
    {{ encore_entry_script_tags('page_workflow_show') }}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_wopi_link') }}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_document_action_buttons_group') }}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_workflow_attachment') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block css %}
 | 
			
		||||
@@ -20,6 +21,7 @@
 | 
			
		||||
    {{ encore_entry_link_tags('page_workflow_show') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_wopi_link') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_document_action_buttons_group') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_workflow_attachment') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as macro %}
 | 
			
		||||
@@ -58,6 +60,8 @@
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </section>
 | 
			
		||||
 | 
			
		||||
    <section class="step my-4">{% include '@ChillMain/Workflow/_attachment.html.twig' %}</section>
 | 
			
		||||
 | 
			
		||||
    <section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
 | 
			
		||||
    {% if signatures|length > 0 %}
 | 
			
		||||
        <section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user