Merge branch 'calendar/synchro-msgraph' into calendar/finalization

This commit is contained in:
Julien Fastré 2022-06-17 16:52:52 +02:00
commit b7c3300884
389 changed files with 10282 additions and 2770 deletions

View File

@ -18,3 +18,8 @@ max_line_length = 80
[COMMIT_EDITMSG] [COMMIT_EDITMSG]
max_line_length = 0 max_line_length = 0
[*.{js, vue, ts}]
indent_size = 2
indent_style = space

View File

@ -11,19 +11,38 @@ and this project adheres to
## Unreleased ## Unreleased
<!-- write down unreleased development here --> <!-- write down unreleased development here -->
* [person] add civility when creating a person (with the on-the-fly component or in the php form) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557) * [storedobject] add title field on StoredObject entity + use it in activity documents (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/604)
* [person] add address when creating a person (with the on-the-fly component or in the php form) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557) * [main] add a "read more..." on comment embeddable when overflown (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/604)
* [person] add household creation API point (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557) * [person] add closing motive to closed acc course (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/603)
* [person] household filiation: fetch person info when unfolding person (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/586)
* [admin] repair edit of social action in the admin (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/601)
* [admin]: add select2 to Goal form type entity fields (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/702)
* [main] allow hide permissions group list menu (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/577)
* [main] allow hide change user password menu (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/577)
* [main] filter user jobs by active jobs (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/577)
* [main] add civility to User (entity, migration and form type) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/577)
* [admin] refactorisation of the admin section: reorganisation of the menu, translations, form types, new entities (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/592)
* [admin] add admin section for languages and countries (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/596)
* [activity] activity admin: translations + remove label field for comment on admin activity type (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/587)
* [main] admin user_job: improvements (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/588)
* [address] can add extra address info even if noAddress (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/576)
## Test releases ## Test releases
### 2022-05-06
* [person] add civility when creating a person (with the on-the-fly component or in the php form) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557)
* [person] add address when creating a person (with the on-the-fly component or in the php form) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557)
* [person] add household creation API point (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557)
### 2021-04-29 ### 2021-04-29
* [person] prevent circular references in PersonDocGenNormalizer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/527) * [person] prevent circular references in PersonDocGenNormalizer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/527)
* [person] add maritalStatusComment to PersonDocGenNormalizer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/582) * [person] add maritalStatusComment to PersonDocGenNormalizer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/582)
* Load relationships without gender in french fixtures * Load relationships without gender in french fixtures
* Add command to remove old draft accompanying periods * Add command to remove old draft accompanying periods
* [parcours]: If users assings him/herself as referrer and job is not null. Update parcours job (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/578)
### 2021-04-28 ### 2021-04-28

View File

@ -2,7 +2,14 @@
en cours de rédaction en cours de rédaction
## Translations
Par bundle, toutes les traductions des pages twig se trouvent dans un seul fichier `translations/messages.fr.yaml`.
## Emplacement des fichiers
Les controllers, form type & templates twig sont placés à la racine des dossiers `Controller`, `Form` & `Ressources/views`, respectivement. Pour les pages Admin, on ne les mets plus dans des sous-dossiers Admin.
## Assets: nommage des entrypoints ## Assets: nommage des entrypoints
Trois types d'entrypoint: Trois types d'entrypoint:
@ -146,19 +153,20 @@ Ces règles n'ont pas toujours été utilisées par le passé. Elles sont souhai
Les routes sont nommées de cette manière: Les routes sont nommées de cette manière:
`chill_bundle_entite_action` `chill_(api|crud)_bundle_(api)_entite_action`
1. d'abord chill_ (pour tous les modules chill) 1. d'abord chill_ (pour tous les modules chill)
2. ensuite une string qui est identique, par bundle 2. ensuite `crud` ou `api`, optionnel, automatiquement ajouté si la route est générée par la configuration
3. si le point est un point d'api (json), alors ajouter la string `api` 3. ensuite une string qui indique le bundle (`main`, `person`, `activity`, ...)
4. ensuite une string qui indique sur quelle entité porte la route, voire également les sous-entités 4. ensuite, `api`, si la route est une route d'api.
5. ensuite une action (`list`, `view`, `edit`, `new`, ...) 5. ensuite une string qui indique sur quelle entité porte la route, voire également les sous-entités
6. ensuite une action (`list`, `view`, `edit`, `new`, ...)
Le fait d'indiquer `api` en 3 permet de distinguer les routes d'api qui sont générées par la configuration (qui sont toutes préfixées par `chill_api`, de celles générées manuellement. (Exemple: `chill_api_household__index`, et `chill_person_api_household_members_move`) Le fait d'indiquer `api` en quatrième position permet de distinguer les routes d'api qui sont générées par la configuration (qui sont toutes préfixées par `chill_api`, de celles générées manuellement. (Exemple: `chill_api_household__index`, et `chill_person_api_household_members_move`)
Si les points 4 et 5 sont inexistants, alors ils sont remplacés par d'autres éléments de manière à garantir l'unicité de la route, et sa bonne compréhension. Si les points 4 et 5 sont inexistants, alors ils sont remplacés par d'autres éléments de manière à garantir l'unicité de la route, et sa bonne compréhension.
### URL ### Nommage des URL
Les URL respectent également une convention: Les URL respectent également une convention:
@ -219,6 +227,14 @@ Les éléments suivants devraient se trouver dans la liste:
Ces éléments peuvent être entrecoupés de l'identifiant d'une entité. Dans ce cas, cet identifiant se place juste après l'entité auquel il se rapporte. Ces éléments peuvent être entrecoupés de l'identifiant d'une entité. Dans ce cas, cet identifiant se place juste après l'entité auquel il se rapporte.
#### Pour les URL de l'espace Admin
Même conventions que dans les autres pages html de l'application, **mais `admin` est ajouté en deuxième position**. Soit:
`/{_locale}/admin/bundle/entity/{id}/action`
## Règles UI chill ## Règles UI chill
### Titre des pages ### Titre des pages

View File

@ -19,6 +19,7 @@
"graylog2/gelf-php": "^1.5", "graylog2/gelf-php": "^1.5",
"knplabs/knp-menu-bundle": "^3.0", "knplabs/knp-menu-bundle": "^3.0",
"knplabs/knp-time-bundle": "^1.12", "knplabs/knp-time-bundle": "^1.12",
"knpuniversity/oauth2-client-bundle": "^2.10",
"league/csv": "^9.7.1", "league/csv": "^9.7.1",
"nyholm/psr7": "^1.4", "nyholm/psr7": "^1.4",
"ocramius/package-versions": "^1.10 || ^2", "ocramius/package-versions": "^1.10 || ^2",
@ -36,6 +37,7 @@
"symfony/http-foundation": "^4.4", "symfony/http-foundation": "^4.4",
"symfony/intl": "^4.4", "symfony/intl": "^4.4",
"symfony/mailer": "^5.4", "symfony/mailer": "^5.4",
"symfony/messenger": "^5.4",
"symfony/mime": "^5.4", "symfony/mime": "^5.4",
"symfony/monolog-bundle": "^3.5", "symfony/monolog-bundle": "^3.5",
"symfony/security-bundle": "^4.4", "symfony/security-bundle": "^4.4",
@ -45,9 +47,11 @@
"symfony/translation": "^4.4", "symfony/translation": "^4.4",
"symfony/twig-bundle": "^4.4", "symfony/twig-bundle": "^4.4",
"symfony/validator": "^4.4", "symfony/validator": "^4.4",
"symfony/web-link": "*",
"symfony/webpack-encore-bundle": "^1.11", "symfony/webpack-encore-bundle": "^1.11",
"symfony/workflow": "^4.4", "symfony/workflow": "^4.4",
"symfony/yaml": "^4.4", "symfony/yaml": "^4.4",
"thenetworg/oauth2-azure": "^2.0",
"twig/extra-bundle": "^3.0", "twig/extra-bundle": "^3.0",
"twig/intl-extra": "^3.0", "twig/intl-extra": "^3.0",
"twig/markdown-extra": "^3.3", "twig/markdown-extra": "^3.3",

View File

@ -471,11 +471,6 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillMainBundle/Form/Type/UserPickerType.php path: src/Bundle/ChillMainBundle/Form/Type/UserPickerType.php
-
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1
path: src/Bundle/ChillMainBundle/Form/UserType.php
- -
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#" message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 2 count: 2

View File

@ -15,6 +15,7 @@ use Chill\ActivityBundle\Validator\Constraints as ActivityValidator;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable;
use Chill\MainBundle\Entity\HasCenterInterface; use Chill\MainBundle\Entity\HasCenterInterface;
use Chill\MainBundle\Entity\HasScopeInterface; use Chill\MainBundle\Entity\HasScopeInterface;
use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Location;
@ -134,6 +135,11 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
*/ */
private ?Collection $persons = null; private ?Collection $persons = null;
/**
* @ORM\Embedded(class="Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable", columnPrefix="privateComment_")
*/
private PrivateCommentEmbeddable $privateComment;
/** /**
* @ORM\ManyToMany(targetEntity="Chill\ActivityBundle\Entity\ActivityReason") * @ORM\ManyToMany(targetEntity="Chill\ActivityBundle\Entity\ActivityReason")
* @Groups({"docgen:read"}) * @Groups({"docgen:read"})
@ -193,6 +199,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
{ {
$this->reasons = new ArrayCollection(); $this->reasons = new ArrayCollection();
$this->comment = new CommentEmbeddable(); $this->comment = new CommentEmbeddable();
$this->privateComment = new PrivateCommentEmbeddable();
$this->persons = new ArrayCollection(); $this->persons = new ArrayCollection();
$this->thirdParties = new ArrayCollection(); $this->thirdParties = new ArrayCollection();
$this->documents = new ArrayCollection(); $this->documents = new ArrayCollection();
@ -400,6 +407,11 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
return []; return [];
} }
public function getPrivateComment(): PrivateCommentEmbeddable
{
return $this->privateComment;
}
public function getReasons(): Collection public function getReasons(): Collection
{ {
return $this->reasons; return $this->reasons;
@ -586,6 +598,13 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
return $this; return $this;
} }
public function setPrivateComment(PrivateCommentEmbeddable $privateComment): self
{
$this->privateComment = $privateComment;
return $this;
}
public function setReasons(?ArrayCollection $reasons): self public function setReasons(?ArrayCollection $reasons): self
{ {
$this->reasons = $reasons; $this->reasons = $reasons;

View File

@ -167,6 +167,16 @@ class ActivityType
*/ */
private int $personVisible = self::FIELD_REQUIRED; private int $personVisible = self::FIELD_REQUIRED;
/**
* @ORM\Column(type="string", nullable=false, options={"default": ""})
*/
private string $privateCommentLabel = '';
/**
* @ORM\Column(type="smallint", nullable=false, options={"default": 1})
*/
private int $privateCommentVisible = self::FIELD_OPTIONAL;
/** /**
* @ORM\Column(type="string", nullable=false, options={"default": ""}) * @ORM\Column(type="string", nullable=false, options={"default": ""})
*/ */
@ -416,6 +426,16 @@ class ActivityType
return $this->personVisible; return $this->personVisible;
} }
public function getPrivateCommentLabel(): string
{
return $this->privateCommentLabel;
}
public function getPrivateCommentVisible(): int
{
return $this->privateCommentVisible;
}
public function getReasonsLabel(): string public function getReasonsLabel(): string
{ {
return $this->reasonsLabel; return $this->reasonsLabel;
@ -688,6 +708,20 @@ class ActivityType
return $this; return $this;
} }
public function setPrivateCommentLabel(string $privateCommentLabel): self
{
$this->privateCommentLabel = $privateCommentLabel;
return $this;
}
public function setPrivateCommentVisible(int $privateCommentVisible): self
{
$this->privateCommentVisible = $privateCommentVisible;
return $this;
}
public function setReasonsLabel(string $reasonsLabel): self public function setReasonsLabel(string $reasonsLabel): self
{ {
$this->reasonsLabel = $reasonsLabel; $this->reasonsLabel = $reasonsLabel;

View File

@ -20,6 +20,7 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\CommentType; use Chill\MainBundle\Form\Type\CommentType;
use Chill\MainBundle\Form\Type\PrivateCommentType;
use Chill\MainBundle\Form\Type\ScopePickerType; use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\MainBundle\Form\Type\UserPickerType; use Chill\MainBundle\Form\Type\UserPickerType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
@ -251,6 +252,13 @@ class ActivityType extends AbstractType
]); ]);
} }
if ($activityType->isVisible('privateComment')) {
$builder->add('privateComment', PrivateCommentType::class, [
'label' => '' === $activityType->getLabel('privateComment') ? 'private comment' : $activityType->getPrivateCommentLabel(),
'required' => false,
]);
}
if ($activityType->isVisible('persons')) { if ($activityType->isVisible('persons')) {
$builder->add('persons', HiddenType::class); $builder->add('persons', HiddenType::class);
$builder->get('persons') $builder->get('persons')
@ -313,6 +321,7 @@ class ActivityType extends AbstractType
'button_add_label' => 'activity.Insert a document', 'button_add_label' => 'activity.Insert a document',
'button_remove_label' => 'activity.Remove a document', 'button_remove_label' => 'activity.Remove a document',
'empty_collection_explain' => 'No documents', 'empty_collection_explain' => 'No documents',
'entry_options' => ['has_title' => true],
]); ]);
} }

View File

@ -57,7 +57,7 @@ class ActivityTypeType extends AbstractType
$fields = [ $fields = [
'persons', 'user', 'date', 'location', 'persons', 'persons', 'user', 'date', 'location', 'persons',
'thirdParties', 'durationTime', 'travelTime', 'attendee', 'thirdParties', 'durationTime', 'travelTime', 'attendee',
'reasons', 'comment', 'sentReceived', 'documents', 'reasons', 'comment', 'privateComment', 'sentReceived', 'documents',
'emergency', 'socialIssues', 'socialActions', 'users', 'emergency', 'socialIssues', 'socialActions', 'users',
]; ];
@ -69,6 +69,9 @@ class ActivityTypeType extends AbstractType
'empty_data' => '', 'empty_data' => '',
]); ]);
} }
$builder
->add('commentVisible', ActivityFieldPresence::class);
} }
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)

View File

@ -15,8 +15,6 @@ use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use function in_array;
final class AdminMenuBuilder implements LocalMenuBuilderInterface final class AdminMenuBuilder implements LocalMenuBuilderInterface
{ {
private Security $security; private Security $security;
@ -32,27 +30,38 @@ final class AdminMenuBuilder implements LocalMenuBuilderInterface
return; return;
} }
if (in_array($menuId, ['admin_index', 'admin_section'], true)) {
$menu->addChild('Activities', [ $menu->addChild('Activities', [
'route' => 'chill_admin_activity_index', 'route' => 'chill_activity_admin_index',
]) ])
->setAttribute('class', 'list-group-item-header')
->setExtras([ ->setExtras([
'order' => 2000, 'order' => 5000,
'explain' => 'Activity configuration', 'icons' => ['exchange'],
]); ]);
} else {
$menu $menu->addChild('Activity Reasons', [
->addChild('Activities', [ 'route' => 'chill_activity_activityreason',
'route' => 'chill_admin_activity_index', ])->setExtras(['order' => 5010]);
])
->setExtras([ $menu->addChild('Activity Reasons Category', [
'order' => '60', 'route' => 'chill_activity_activityreasoncategory',
]); ])->setExtras(['order' => 5020]);
}
$menu->addChild('Activity type', [
'route' => 'chill_activity_type_admin',
])->setExtras(['order' => 5030]);
$menu->addChild('Activity Presences', [
'route' => 'chill_crud_activity_presence_index',
])->setExtras(['order' => 5040]);
$menu->addChild('Activity Types Categories', [
'route' => 'chill_activity_type_category_admin',
])->setExtras(['order' => 5050]);
} }
public static function getMenuIds(): array public static function getMenuIds(): array
{ {
return ['admin_index', 'admin_section', 'admin_activity']; return ['admin_section', 'admin_activity'];
} }
} }

View File

@ -18,7 +18,7 @@
{% endmacro %} {% endmacro %}
{% set blocks = [] %} {% set blocks = [] %}
{% if entity.activityType.personsVisible %} {% if context == 'calendar_accompanyingCourse' or entity.activityType.personsVisible %}
{% if context == 'person' %} {% if context == 'person' %}
{% set blocks = blocks|merge([{ {% set blocks = blocks|merge([{
'title': 'Others persons'|trans, 'title': 'Others persons'|trans,
@ -43,7 +43,7 @@
}]) %} }]) %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if entity.activityType.thirdPartiesVisible %} {% if context == 'calendar_accompanyingCourse' or entity.activityType.thirdPartiesVisible %}
{% set blocks = blocks|merge([{ {% set blocks = blocks|merge([{
'title': 'Third parties'|trans, 'title': 'Third parties'|trans,
'items': entity.thirdParties, 'items': entity.thirdParties,
@ -52,7 +52,7 @@
'key' : 'id', 'key' : 'id',
}]) %} }]) %}
{% endif %} {% endif %}
{% if entity.activityType.usersVisible %} {% if context == 'calendar_accompanyingCourse' or entity.activityType.usersVisible %}
{% set blocks = blocks|merge([{ {% set blocks = blocks|merge([{
'title': 'Users concerned'|trans, 'title': 'Users concerned'|trans,
'items': entity.users, 'items': entity.users,

View File

@ -83,6 +83,10 @@
{{ form_row(edit_form.comment) }} {{ form_row(edit_form.comment) }}
{% endif %} {% endif %}
{%- if edit_form.privateComment is defined -%}
{{ form_row(edit_form.privateComment) }}
{% endif %}
{%- if edit_form.attendee is defined -%} {%- if edit_form.attendee is defined -%}
{{ form_row(edit_form.attendee) }} {{ form_row(edit_form.attendee) }}
{% endif %} {% endif %}

View File

@ -81,10 +81,13 @@
{% endif %} {% endif %}
{%- if form.comment is defined -%} {%- if form.comment is defined -%}
{# TODO .. public and private #}
{{ form_row(form.comment) }} {{ form_row(form.comment) }}
{% endif %} {% endif %}
{%- if form.privateComment is defined -%}
{{ form_row(form.privateComment) }}
{% endif %}
{%- if form.attendee is defined -%} {%- if form.attendee is defined -%}
{{ form_row(form.attendee) }} {{ form_row(form.attendee) }}
{% endif %} {% endif %}

View File

@ -1,4 +1,5 @@
{%- set t = entity.type -%} {%- set t = entity.type -%}
{% set userId = app.user.id %}
{%- import "@ChillDocStore/Macro/macro.html.twig" as m -%} {%- import "@ChillDocStore/Macro/macro.html.twig" as m -%}
<h1>{{ "Activity"|trans }}</h1> <h1>{{ "Activity"|trans }}</h1>
@ -146,13 +147,28 @@
</dd> </dd>
{% endif %} {% endif %}
{% if t.privateCommentVisible and is_granted('CHILL_ACTIVITY_SEE_DETAILS', entity) and entity.privateComment.hasCommentForUser(app.user) %}
{% if t.privateCommentLabel is not empty %}
<dt class="inline">{{ t.privateCommentLabel }}</dt>
{% else %}
<dt class="inline">{{ 'Private comment'|trans }}</dt>
{% endif %}
<dd>
<section class="chill-entity entity-comment-embeddable">
<blockquote class="chill-user-quote private-quote">
{{ entity.privateComment.comments[userId] }}
</blockquote>
</section>
</dd>
{% endif %}
{% if t.documentsVisible and is_granted('CHILL_ACTIVITY_SEE_DETAILS', entity) %} {% if t.documentsVisible and is_granted('CHILL_ACTIVITY_SEE_DETAILS', entity) %}
<dt class="inline">{{ 'Documents'|trans }}</dt> <dt class="inline">{{ 'Documents'|trans }}</dt>
<dd> <dd>
{% if entity.documents|length > 0 %} {% if entity.documents|length > 0 %}
<ul> <ul>
{% for d in entity.documents %} {% for d in entity.documents %}
<li>{{ m.download_button(d) }}</li> <li>{{ d.title }}{{ m.download_button(d) }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}

View File

@ -1,10 +1,10 @@
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %} {% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %} {% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %} {% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %} {% endblock %}
{% block layout_wvm_content %} {% block admin_content %}
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %} {% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{% block content_form_actions_view %}{% endblock %} {% block content_form_actions_view %}{% endblock %}
{% block content_form_actions_save_and_show %}{% endblock %} {% block content_form_actions_save_and_show %}{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %} {% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityPresence list'|trans }}</h1> <h1>{{ 'ActivityPresence list'|trans }}</h1>
@ -34,7 +34,7 @@
</tbody> </tbody>
</table> </table>
<ul class="record_actions"> <ul class="record_actions sticky-form-buttons">
<li> <li>
<a href="{{ path('chill_crud_activity_presence_new') }}" class="btn btn-create"> <a href="{{ path('chill_crud_activity_presence_new') }}" class="btn btn-create">
{{ 'Create a new activity presence'|trans }} {{ 'Create a new activity presence'|trans }}

View File

@ -1,10 +1,10 @@
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %} {% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %} {% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %} {% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %} {% endblock %}
{% block layout_wvm_content %} {% block admin_content %}
{% embed '@ChillMain/CRUD/_new_content.html.twig' %} {% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %} {% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %} {% endembed %}

View File

@ -1,20 +1,4 @@
{# {% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityReason edit'|trans }}</h1> <h1>{{ 'ActivityReason edit'|trans }}</h1>
@ -24,7 +8,7 @@
{{ form_row(edit_form.active) }} {{ form_row(edit_form.active) }}
{{ form_row(edit_form.category) }} {{ form_row(edit_form.category) }}
<ul class="record_actions"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">
<a href="{{ path('chill_activity_activityreason') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a> <a href="{{ path('chill_activity_activityreason') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li> </li>

View File

@ -1,20 +1,4 @@
{# {% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityReason list'|trans }}</h1> <h1>{{ 'ActivityReason list'|trans }}</h1>
@ -45,7 +29,11 @@
</tbody> </tbody>
</table> </table>
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class='cancel'>
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a>
</li>
<li> <li>
<a href="{{ path('chill_activity_activityreason_new') }}" class="btn btn-new"> <a href="{{ path('chill_activity_activityreason_new') }}" class="btn btn-new">
{{ 'Create a new activity reason'|trans }} {{ 'Create a new activity reason'|trans }}

View File

@ -1,20 +1,4 @@
{# {% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityReason creation'|trans }}</h1> <h1>{{ 'ActivityReason creation'|trans }}</h1>
@ -24,7 +8,7 @@
{{ form_row(form.active) }} {{ form_row(form.active) }}
{{ form_row(form.category) }} {{ form_row(form.category) }}
<ul class="record_actions"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">
<a href="{{ path('chill_activity_activityreason') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a> <a href="{{ path('chill_activity_activityreason') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li> </li>

View File

@ -1,20 +1,4 @@
{# {% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityReason'|trans }}</h1> <h1>{{ 'ActivityReason'|trans }}</h1>
@ -38,7 +22,7 @@
</tbody> </tbody>
</table> </table>
<ul class="record_actions"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">
<a href="{{ path('chill_activity_activityreason') }}" class="btn btn-cancel"> <a href="{{ path('chill_activity_activityreason') }}" class="btn btn-cancel">
{{ 'Back to the list'|trans }} {{ 'Back to the list'|trans }}

View File

@ -1,20 +1,4 @@
{# {% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityReasonCategory edit'|trans }}</h1> <h1>{{ 'ActivityReasonCategory edit'|trans }}</h1>
@ -23,7 +7,7 @@
{{ form_row(edit_form.name) }} {{ form_row(edit_form.name) }}
{{ form_row(edit_form.active) }} {{ form_row(edit_form.active) }}
<ul class="record_actions"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">
<a href="{{ path('chill_activity_activityreasoncategory') }}" class="btn btn-cancel"> <a href="{{ path('chill_activity_activityreasoncategory') }}" class="btn btn-cancel">
{{ 'Back to the list'|trans }} {{ 'Back to the list'|trans }}

View File

@ -1,20 +1,4 @@
{# {% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityReasonCategory list'|trans }}</h1> <h1>{{ 'ActivityReasonCategory list'|trans }}</h1>
@ -46,7 +30,7 @@
</tbody> </tbody>
</table> </table>
<ul class="record_actions"> <ul class="record_actions sticky-form-buttons">
<li> <li>
<a href="{{ path('chill_activity_activityreasoncategory_new') }}" class="btn btn-new"> <a href="{{ path('chill_activity_activityreasoncategory_new') }}" class="btn btn-new">
{{ 'Create a new activity category reason'|trans }} {{ 'Create a new activity category reason'|trans }}

View File

@ -1,20 +1,4 @@
{# {% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityReasonCategory creation'|trans }}</h1> <h1>{{ 'ActivityReasonCategory creation'|trans }}</h1>
@ -23,7 +7,7 @@
{{ form_row(form.name) }} {{ form_row(form.name) }}
{{ form_row(form.active) }} {{ form_row(form.active) }}
<ul class="record_actions"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">
<a href="{{ path('chill_activity_activityreasoncategory') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a> <a href="{{ path('chill_activity_activityreasoncategory') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li> </li>

View File

@ -1,20 +1,4 @@
{# {% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityReasonCategory'|trans }}</h1> <h1>{{ 'ActivityReasonCategory'|trans }}</h1>
@ -37,7 +21,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<ul class="record_actions"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">
<a href="{{ path('chill_activity_activityreasoncategory') }}" class="btn btn-cancel"> <a href="{{ path('chill_activity_activityreasoncategory') }}" class="btn btn-cancel">
{{ 'Back to the list'|trans }} {{ 'Back to the list'|trans }}

View File

@ -1,10 +1,10 @@
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %} {% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %} {% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %} {% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %} {% endblock %}
{% block layout_wvm_content %} {% block admin_content %}
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %} {% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{% block content_form_actions_view %}{% endblock %} {% block content_form_actions_view %}{% endblock %}
{% block content_form_actions_save_and_show %}{% endblock %} {% block content_form_actions_save_and_show %}{% endblock %}

View File

@ -1,20 +1,4 @@
{# {% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityType list'|trans }}</h1> <h1>{{ 'ActivityType list'|trans }}</h1>
@ -50,7 +34,7 @@
</tbody> </tbody>
</table> </table>
<ul class="record_actions"> <ul class="record_actions sticky-form-buttons">
<li> <li>
<a href="{{ path('chill_crud_activity_type_new') }}" class="btn btn-create"> <a href="{{ path('chill_crud_activity_type_new') }}" class="btn btn-create">
{{ 'Create a new activity type'|trans }} {{ 'Create a new activity type'|trans }}

View File

@ -1,10 +1,10 @@
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %} {% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %} {% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %} {% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %} {% endblock %}
{% block layout_wvm_content %} {% block admin_content %}
{% embed '@ChillMain/CRUD/_new_content.html.twig' %} {% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %} {% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %} {% endembed %}

View File

@ -1,10 +1,10 @@
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %} {% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %} {% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %} {% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %} {% endblock %}
{% block layout_wvm_content %} {% block admin_content %}
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %} {% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{% block content_form_actions_view %}{% endblock %} {% block content_form_actions_view %}{% endblock %}
{% block content_form_actions_save_and_show %}{% endblock %} {% block content_form_actions_save_and_show %}{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %} {% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityTypeCategory list'|trans }}</h1> <h1>{{ 'ActivityTypeCategory list'|trans }}</h1>
@ -34,7 +34,7 @@
</tbody> </tbody>
</table> </table>
<ul class="record_actions"> <ul class="record_actions sticky-form-buttons">
<li> <li>
<a href="{{ path('chill_crud_activity_type_category_new') }}" class="btn btn-create"> <a href="{{ path('chill_crud_activity_type_category_new') }}" class="btn btn-create">
{{ 'Create a new activity type category'|trans }} {{ 'Create a new activity type category'|trans }}

View File

@ -1,10 +1,10 @@
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %} {% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %} {% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %} {% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %} {% endblock %}
{% block layout_wvm_content %} {% block admin_content %}
{% embed '@ChillMain/CRUD/_new_content.html.twig' %} {% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %} {% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %} {% endembed %}

View File

@ -20,7 +20,7 @@
{% block vertical_menu_content %} {% block vertical_menu_content %}
{{ chill_menu('admin_activity', { {{ chill_menu('admin_activity', {
'layout': '@ChillActivity/Admin/menu_activity.html.twig', 'layout': '@ChillMain/Admin/menu_admin_section.html.twig',
}) }} }) }}
{% endblock %} {% endblock %}

View File

@ -1,20 +0,0 @@
{#
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
<info@champs-libres.coop> / <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "@ChillMain/Menu/verticalMenu.html.twig" %}
{% block v_menu_title %}{{ 'Activity configuration menu'|trans }}{% endblock %}

View File

@ -209,6 +209,7 @@ class ActivityContext implements
*/ */
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
{ {
$storedObject->setTitle($this->translatableStringHelper->localize($template->getName()));
$entity->addDocument($storedObject); $entity->addDocument($storedObject);
$this->em->persist($storedObject); $this->em->persist($storedObject);

View File

@ -10,48 +10,22 @@ chill_activity_activityreasoncategory:
resource: "@ChillActivityBundle/config/routes/activityreasoncategory.yaml" resource: "@ChillActivityBundle/config/routes/activityreasoncategory.yaml"
prefix: / prefix: /
chill_admin_activity_index: chill_activity_admin_index:
path: /{_locale}/admin/activity path: /{_locale}/admin/activity
controller: Chill\ActivityBundle\Controller\AdminController::indexActivityAction controller: Chill\ActivityBundle\Controller\AdminController::indexActivityAction
options:
menus:
admin_section:
label: Activities
order: 2000
icons: [tag]
chill_admin_activity_redirect_to_admin_index: chill_admin_activity_redirect_to_admin_index:
path: /{_locale}/admin/activity_redirect_to_main path: /{_locale}/admin/activity_redirect_to_main
controller: Chill\ActivityBundle\Controller\AdminController::redirectToAdminIndexAction controller: Chill\ActivityBundle\Controller\AdminController::redirectToAdminIndexAction
options:
menus:
admin_activity:
order: 0
label: Main admin menu
chill_activity_type_admin: chill_activity_type_admin:
path: /{_locale}/admin/activity/type path: /{_locale}/admin/activity/type
controller: cscrud_activity_type_controller:index controller: cscrud_activity_type_controller:index
options:
menus:
admin_activity:
order: 2020
label: 'Activity Types'
chill_activity_type_category_admin: chill_activity_type_category_admin:
path: /{_locale}/admin/activity/type_category path: /{_locale}/admin/activity/type_category
controller: cscrud_activity_type_category_controller:index controller: cscrud_activity_type_category_controller:index
options:
menus:
admin_activity:
order: 2999
label: 'Activity Types Categories'
chill_activity_presence_admin: chill_activity_presence_admin:
path: /{_locale}/admin/activity/presence path: /{_locale}/admin/activity/presence
controller: cscrud_activity_presence_controller:index controller: cscrud_activity_presence_controller:index
options:
menus:
admin_activity:
order: 2021
label: 'Activity Presences'

View File

@ -1,11 +1,6 @@
chill_activity_activityreason: chill_activity_activityreason:
path: /{_locale}/admin/activityreason/ path: /{_locale}/admin/activityreason/
controller: Chill\ActivityBundle\Controller\ActivityReasonController::indexAction controller: Chill\ActivityBundle\Controller\ActivityReasonController::indexAction
options:
menus:
admin_activity:
order: 2000
label: "Activity Reasons"
chill_activity_activityreason_show: chill_activity_activityreason_show:
path: /{_locale}/admin/activityreason/{id}/show path: /{_locale}/admin/activityreason/{id}/show

View File

@ -1,11 +1,6 @@
chill_activity_activityreasoncategory: chill_activity_activityreasoncategory:
path: /{_locale}/admin/activityreasoncategory/ path: /{_locale}/admin/activityreasoncategory/
controller: Chill\ActivityBundle\Controller\ActivityReasonCategoryController::indexAction controller: Chill\ActivityBundle\Controller\ActivityReasonCategoryController::indexAction
options:
menus:
admin_activity:
order: 2010
label: "Activity Reasons Category"
chill_activity_activityreasoncategory_show: chill_activity_activityreasoncategory_show:
path: /{_locale}/admin/activityreasoncategory/{id}/show path: /{_locale}/admin/activityreasoncategory/{id}/show

View File

@ -0,0 +1,35 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\Migrations\Activity;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220425133027 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE activitytype DROP privateCommentLabel');
$this->addSql('ALTER TABLE activitytype DROP privateCommentVisible');
}
public function getDescription(): string
{
return 'add private comment option to activity types';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE activitytype ADD privateCommentLabel VARCHAR(255) DEFAULT \'\' NOT NULL');
$this->addSql('ALTER TABLE activitytype ADD privateCommentVisible SMALLINT DEFAULT 1 NOT NULL');
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\Migrations\Activity;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220527124438 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_accompanying_period_work DROP privateComment_comments');
}
public function getDescription(): string
{
return 'add private comment to activity';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE activity ADD privateComment_comments JSON DEFAULT \'{}\'');
}
}

View File

@ -44,6 +44,7 @@ Received: Recevoir
by: 'Par ' by: 'Par '
location: Lieu location: Lieu
Reasons: Sujets Reasons: Sujets
Private comment: Commentaire privé
#forms #forms
@ -113,7 +114,7 @@ Activity type configuration: Configuration des categories d'activités
Activity Reasons: Sujets d'une activité Activity Reasons: Sujets d'une activité
Activity Reasons Category: Catégories de sujet d'activités Activity Reasons Category: Catégories de sujet d'activités
Activity Types Categories: Catégories des types d'activité Activity Types Categories: Catégories des types d'activité
Activity Presences: Presences des activités Activity Presences: Presences aux activités
# Crud # Crud
@ -124,6 +125,9 @@ crud:
activity_type_category: activity_type_category:
title_new: Nouvelle catégorie de type d'activité title_new: Nouvelle catégorie de type d'activité
title_edit: Edition d'une catégorie de type d'activité title_edit: Edition d'une catégorie de type d'activité
activity_presence:
title_new: Nouvelle Présence aux activités
title_edit: Edition d'une Présence aux activités
# activity reason admin # activity reason admin
ActivityReason list: Liste des sujets ActivityReason list: Liste des sujets
@ -145,6 +149,10 @@ ActivityReasonCategory: Catégorie de sujet d'activité
ActivityReasonCategory is active and will be proposed: La catégorie est active et sera proposée ActivityReasonCategory is active and will be proposed: La catégorie est active et sera proposée
ActivityReasonCategory is inactive and won't be proposed: La catégorie est inactive et ne sera pas proposée ActivityReasonCategory is inactive and won't be proposed: La catégorie est inactive et ne sera pas proposée
#activity presence admin
ActivityPresence list: Liste des Présences aux activités
Create a new activity presence: Créer une nouvelle "Présence aux activités"
# activity type type admin # activity type type admin
ActivityType list: Types d'activités ActivityType list: Types d'activités
Create a new activity type: Créer un nouveau type d'activité Create a new activity type: Créer un nouveau type d'activité
@ -168,6 +176,8 @@ Reasons visible: Visibilité du champ Sujet
Reasons label: Libellé du champ Sujet Reasons label: Libellé du champ Sujet
Comment visible: Visibilité du champ Commentaire Comment visible: Visibilité du champ Commentaire
Comment label: Libellé du champ Commentaire Comment label: Libellé du champ Commentaire
Private comment visible: Visibilité du champ Commentaire Privé
Private comment label: Libellé du champ Commentaire Privé
Emergency visible: Visibilité du champ Urgent Emergency visible: Visibilité du champ Urgent
Emergency label: Libellé du champ Urgent Emergency label: Libellé du champ Urgent
Accompanying period visible: Visibilité du champ Période d'accompagnement Accompanying period visible: Visibilité du champ Période d'accompagnement

View File

@ -0,0 +1,29 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\AsideActivityBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
/**
* Class AdminController.
*/
class AdminController extends AbstractController
{
/**
* @Route("/{_locale}/admin/aside-activity", name="chill_aside_activity_admin")
*/
public function indexAdminAction()
{
return $this->render('ChillAsideActivityBundle:Admin:index.html.twig');
}
}

View File

@ -14,8 +14,6 @@ namespace Chill\AsideActivityBundle\Menu;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use function in_array;
final class AdminMenuBuilder implements \Chill\MainBundle\Routing\LocalMenuBuilderInterface final class AdminMenuBuilder implements \Chill\MainBundle\Routing\LocalMenuBuilderInterface
{ {
private Security $security; private Security $security;
@ -32,27 +30,25 @@ final class AdminMenuBuilder implements \Chill\MainBundle\Routing\LocalMenuBuild
return; return;
} }
if (in_array($menuId, ['admin_index', 'admin_section'], true)) {
$menu->addChild('Aside activities', [ $menu->addChild('Aside activities', [
'route' => 'chill_crud_aside_activity_category_index', 'route' => 'chill_aside_activity_admin',
]) ])
->setAttribute('class', 'list-group-item-header')
->setExtras([ ->setExtras([
'order' => 900, 'order' => 7000,
'explain' => 'Aside activity type configuration', 'explain' => 'Aside activity type configuration',
]); ]);
} else {
$menu $menu
->addChild('Aside activity categories', [ ->addChild('Aside activity categories', [
'route' => 'chill_crud_aside_activity_category_index', 'route' => 'chill_crud_aside_activity_category_index',
]) ])
->setExtras([ ->setExtras([
'order' => '50', 'order' => 7010,
]); ]);
} }
}
public static function getMenuIds(): array public static function getMenuIds(): array
{ {
return ['admin_index', 'admin_section', 'admin_aside_activity']; return ['admin_section', 'admin_aside_activity'];
} }
} }

View File

@ -1,7 +1,9 @@
{% extends "@ChillMain/Admin/layoutWithVerticalMenu.html.twig" %} {% extends "@ChillMain/Admin/layoutWithVerticalMenu.html.twig" %}
{% block vertical_menu_content %} {% block vertical_menu_content %}
{{ chill_menu('admin_aside_activity') }} {{ chill_menu('admin_aside_activity', {
'layout': '@ChillMain/Admin/menu_admin_section.html.twig',
}) }}
{% endblock %} {% endblock %}
{% block layout_wvm_content %} {% block layout_wvm_content %}

View File

@ -1,10 +1,10 @@
{% extends "@ChillAsideActivity/Admin/layout_asideactivity.html.twig" %} {% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %} {% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %} {% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %} {% endblock %}
{% block layout_wvm_content %} {% block admin_content %}
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %} {% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{% block content_form_actions_view %}{% endblock %} {% block content_form_actions_view %}{% endblock %}
{% block content_form_actions_save_and_show %}{% endblock %} {% block content_form_actions_save_and_show %}{% endblock %}

View File

@ -1,6 +1,9 @@
{% extends "@ChillAsideActivity/Admin/layout_asideactivity.html.twig" %} {% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
{% block title %}{{ 'Aside Activity Type List'|trans }}{% endblock title %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'Aside Activity Type List'|trans }}</h1> <h1>{{ 'Aside Activity Type List'|trans }}</h1>
<table class="records_list table table-bordered border-dark"> <table class="records_list table table-bordered border-dark">
@ -36,7 +39,7 @@
</tbody> </tbody>
</table> </table>
<ul class="record_actions"> <ul class="record_actions sticky-form-buttons">
<li> <li>
<a href="{{ path('chill_crud_aside_activity_category_new') }}" class="btn btn-create"> <a href="{{ path('chill_crud_aside_activity_category_new') }}" class="btn btn-create">
{{ 'Create a new aside activity type'|trans }} {{ 'Create a new aside activity type'|trans }}

View File

@ -1,10 +1,10 @@
{% extends "@ChillAsideActivity/Admin/layout_asideactivity.html.twig" %} {% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %} {% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %} {% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %} {% endblock %}
{% block layout_wvm_content %} {% block admin_content %}
{% embed '@ChillMain/CRUD/_new_content.html.twig' %} {% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %} {% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %} {% endembed %}

View File

@ -38,7 +38,7 @@ crud:
confirm_message_delete: Êtes-vous sûr de vouloir supprimer cette activité annexe? confirm_message_delete: Êtes-vous sûr de vouloir supprimer cette activité annexe?
aside_activity_category: aside_activity_category:
title_new: Nouvelle catégorie d'activité annexe title_new: Nouvelle catégorie d'activité annexe
title_edit: Edition d'une catégorie de type d'activité title_edit: Édition d'une catégorie de type d'activité
#forms #forms
Create a new aside activity type: Nouvelle categorie d'activité annexe Create a new aside activity type: Nouvelle categorie d'activité annexe
@ -165,3 +165,4 @@ Phonecall: "Appel téléphonique"
Aside activities: Activités annexes Aside activities: Activités annexes
Aside activity types: Types d'activités annexes Aside activity types: Types d'activités annexes
Aside activity type configuration: Configuration des categories d'activités annexes Aside activity type configuration: Configuration des categories d'activités annexes
Aside activity configuration: Configuration des activités annexes

View File

@ -11,8 +11,16 @@ declare(strict_types=1);
namespace Chill\CalendarBundle; namespace Chill\CalendarBundle;
use Chill\CalendarBundle\RemoteCalendar\DependencyInjection\RemoteCalendarCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
class ChillCalendarBundle extends Bundle class ChillCalendarBundle extends Bundle
{ {
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new RemoteCalendarCompilerPass());
}
} }

View File

@ -0,0 +1,77 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Command;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineTokenStorage;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use TheNetworg\OAuth2\Client\Provider\Azure;
class AzureGrantAdminConsentAndAcquireToken extends Command
{
private Azure $azure;
private ClientRegistry $clientRegistry;
private MachineTokenStorage $machineTokenStorage;
public function __construct(Azure $azure, ClientRegistry $clientRegistry, MachineTokenStorage $machineTokenStorage)
{
parent::__construct('chill:calendar:msgraph-grant-admin-consent');
$this->azure = $azure;
$this->clientRegistry = $clientRegistry;
$this->machineTokenStorage = $machineTokenStorage;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
/** @var FormatterHelper $formatter */
$formatter = $this->getHelper('formatter');
$this->azure->scope = ['https://graph.microsoft.com/.default'];
$authorizationUrl = explode('?', $this->azure->getAuthorizationUrl(['prompt' => 'admin_consent']));
// replace the first part by the admin consent authorization url
$authorizationUrl[0] = strtr('https://login.microsoftonline.com/{tenant}/adminconsent', ['{tenant}' => $this->azure->tenant]);
$output->writeln('Go to the url');
$output->writeln(implode('?', $authorizationUrl));
$output->writeln('Authenticate as admin, and grant admin consent');
// not necessary ?
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Access granted ?');
if (!$helper->ask($input, $output, $question)) {
$messages = ['No problem, we will wait for you', 'Grant access and come back here'];
$output->writeln($formatter->formatBlock($messages, 'warning'));
return 0;
}
$token = $this->machineTokenStorage->getToken();
$messages = ['Token acquired!', 'We could acquire a machine token successfully'];
$output->writeln($formatter->formatBlock($messages, 'success'));
$output->writeln('Token information:');
$output->writeln($token->getToken());
$output->writeln('Expires at: ' . $token->getExpires());
$output->writeln('To inspect the token content, go to https://jwt.ms/#access_token=' . urlencode($token->getToken()));
return 0;
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Command;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
use Chill\MainBundle\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MapUserCalendarCommand extends Command
{
private EntityManagerInterface $em;
private MapCalendarToUser $mapCalendarToUser;
private UserRepository $userRepository;
public function __construct(EntityManagerInterface $em, MapCalendarToUser $mapCalendarToUser, UserRepository $userRepository)
{
parent::__construct('chill:calendar:map-user');
$this->em = $em;
$this->mapCalendarToUser = $mapCalendarToUser;
$this->userRepository = $userRepository;
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$limit = 2;
$offset = 0;
$total = $this->userRepository->countByNotHavingAttribute(MapCalendarToUser::METADATA_KEY);
while ($offset < $total) {
$users = $this->userRepository->findByNotHavingAttribute(MapCalendarToUser::METADATA_KEY, $limit, $offset);
foreach ($users as $user) {
$this->mapCalendarToUser->writeMetadata($user);
++$offset;
}
$this->em->flush();
$this->em->clear();
}
return 0;
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class AdminController extends AbstractController
{
/**
* Calendar admin.
*
* @Route("/{_locale}/admin/calendar", name="chill_calendar_admin_index")
*/
public function indexAdminAction()
{
return $this->render('ChillCalendarBundle:Admin:index.html.twig');
}
}

View File

@ -13,9 +13,11 @@ namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Form\CalendarType; use Chill\CalendarBundle\Form\CalendarType;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Chill\CalendarBundle\Repository\CalendarRepository; use Chill\CalendarBundle\Repository\CalendarRepository;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
@ -35,32 +37,40 @@ use Symfony\Component\Serializer\SerializerInterface;
class CalendarController extends AbstractController class CalendarController extends AbstractController
{ {
protected AuthorizationHelper $authorizationHelper; private AuthorizationHelper $authorizationHelper;
protected EventDispatcherInterface $eventDispatcher;
protected LoggerInterface $logger;
protected PaginatorFactory $paginator;
protected SerializerInterface $serializer;
private CalendarRepository $calendarRepository; private CalendarRepository $calendarRepository;
private EventDispatcherInterface $eventDispatcher;
private LoggerInterface $logger;
private PaginatorFactory $paginator;
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
private SerializerInterface $serializer;
private UserRepository $userRepository;
public function __construct( public function __construct(
EventDispatcherInterface $eventDispatcher,
AuthorizationHelper $authorizationHelper, AuthorizationHelper $authorizationHelper,
CalendarRepository $calendarRepository,
EventDispatcherInterface $eventDispatcher,
LoggerInterface $logger, LoggerInterface $logger,
SerializerInterface $serializer,
PaginatorFactory $paginator, PaginatorFactory $paginator,
CalendarRepository $calendarRepository RemoteCalendarConnectorInterface $remoteCalendarConnector,
SerializerInterface $serializer,
UserRepository $userRepository
) { ) {
$this->eventDispatcher = $eventDispatcher;
$this->authorizationHelper = $authorizationHelper; $this->authorizationHelper = $authorizationHelper;
$this->logger = $logger;
$this->serializer = $serializer;
$this->paginator = $paginator;
$this->calendarRepository = $calendarRepository; $this->calendarRepository = $calendarRepository;
$this->eventDispatcher = $eventDispatcher;
$this->logger = $logger;
$this->paginator = $paginator;
$this->remoteCalendarConnector = $remoteCalendarConnector;
$this->serializer = $serializer;
$this->userRepository = $userRepository;
} }
/** /**
@ -127,8 +137,12 @@ class CalendarController extends AbstractController
* *
* @Route("/{_locale}/calendar/calendar/{id}/edit", name="chill_calendar_calendar_edit") * @Route("/{_locale}/calendar/calendar/{id}/edit", name="chill_calendar_calendar_edit")
*/ */
public function editAction(int $id, Request $request): Response public function editAction(Calendar $entity, Request $request): Response
{ {
if (!$this->remoteCalendarConnector->isReady()) {
return $this->remoteCalendarConnector->getMakeReadyResponse($request->getUri());
}
$view = null; $view = null;
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
@ -137,21 +151,14 @@ class CalendarController extends AbstractController
if ($accompanyingPeriod instanceof AccompanyingPeriod) { if ($accompanyingPeriod instanceof AccompanyingPeriod) {
$view = '@ChillCalendar/Calendar/editByAccompanyingCourse.html.twig'; $view = '@ChillCalendar/Calendar/editByAccompanyingCourse.html.twig';
} elseif ($user instanceof User) { } elseif ($user instanceof User) {
throw new Exception('to analyze');
$view = '@ChillCalendar/Calendar/editByUser.html.twig'; $view = '@ChillCalendar/Calendar/editByUser.html.twig';
} }
$entity = $em->getRepository(\Chill\CalendarBundle\Entity\Calendar::class)->find($id); $form = $this->createForm(CalendarType::class, $entity);
$form->handleRequest($request);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Calendar entity.');
}
$form = $this->createForm(CalendarType::class, $entity, [
'accompanyingPeriod' => $accompanyingPeriod,
])->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$em->persist($entity);
$em->flush(); $em->flush();
$this->addFlash('success', $this->get('translator')->trans('Success : calendar item updated!')); $this->addFlash('success', $this->get('translator')->trans('Success : calendar item updated!'));
@ -165,7 +172,7 @@ class CalendarController extends AbstractController
$this->addFlash('error', $this->get('translator')->trans('This form contains errors')); $this->addFlash('error', $this->get('translator')->trans('This form contains errors'));
} }
$deleteForm = $this->createDeleteForm($id, $user, $accompanyingPeriod); $deleteForm = $this->createDeleteForm($entity->getId(), $user, $accompanyingPeriod);
if (null === $view) { if (null === $view) {
throw $this->createNotFoundException('Template not found'); throw $this->createNotFoundException('Template not found');
@ -178,7 +185,7 @@ class CalendarController extends AbstractController
'form' => $form->createView(), 'form' => $form->createView(),
'delete_form' => $deleteForm->createView(), 'delete_form' => $deleteForm->createView(),
'accompanyingCourse' => $accompanyingPeriod, 'accompanyingCourse' => $accompanyingPeriod,
'user' => $user, // 'user' => $user,
'entity_json' => $entity_array, 'entity_json' => $entity_array,
]); ]);
} }
@ -251,6 +258,28 @@ class CalendarController extends AbstractController
throw new Exception('Unable to list actions.'); throw new Exception('Unable to list actions.');
} }
/**
* @Route("/{_locale}/calendar/calendar/my", name="chill_calendar_calendar_list_my")
*/
public function myCalendar(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_USER');
if (!$this->getUser() instanceof User) {
throw new UnauthorizedHttpException('you are not an user');
}
if (!$this->remoteCalendarConnector->isReady()) {
return $this->remoteCalendarConnector->getMakeReadyResponse($request->getUri());
}
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
return $this->render($view, [
'user' => $this->getUser(),
]);
}
/** /**
* Create a new calendar item. * Create a new calendar item.
* *
@ -258,6 +287,10 @@ class CalendarController extends AbstractController
*/ */
public function newAction(Request $request): Response public function newAction(Request $request): Response
{ {
if (!$this->remoteCalendarConnector->isReady()) {
return $this->remoteCalendarConnector->getMakeReadyResponse($request->getUri());
}
$view = null; $view = null;
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
@ -271,8 +304,10 @@ class CalendarController extends AbstractController
// } // }
$entity = new Calendar(); $entity = new Calendar();
$entity->setUser($this->getUser());
$entity->setStatus($entity::STATUS_VALID); if ($request->query->has('mainUser')) {
$entity->setMainUser($this->userRepository->find($request->query->getInt('mainUser')));
}
// if ($user instanceof User) { // if ($user instanceof User) {
// $entity->setPerson($user); // $entity->setPerson($user);
@ -282,9 +317,8 @@ class CalendarController extends AbstractController
$entity->setAccompanyingPeriod($accompanyingPeriod); $entity->setAccompanyingPeriod($accompanyingPeriod);
} }
$form = $this->createForm(CalendarType::class, $entity, [ $form = $this->createForm(CalendarType::class, $entity);
'accompanyingPeriod' => $accompanyingPeriod, $form->handleRequest($request);
])->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$em->persist($entity); $em->persist($entity);

View File

@ -11,38 +11,72 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Controller; namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Serializer\Model\Collection;
use DateTimeImmutable;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use function count; use Symfony\Component\Routing\Annotation\Route;
class CalendarRangeAPIController extends ApiController class CalendarRangeAPIController extends ApiController
{ {
/** private CalendarRangeRepository $calendarRangeRepository;
* @Route("/api/1.0/calendar/calendar-range-available.{_format}", name="chill_api_single_calendar_range_available")
*/ public function __construct(CalendarRangeRepository $calendarRangeRepository)
public function availableRanges(Request $request, string $_format): JsonResponse
{ {
$em = $this->getDoctrine()->getManager(); $this->calendarRangeRepository = $calendarRangeRepository;
$sql = 'SELECT c FROM ChillCalendarBundle:CalendarRange c
WHERE NOT EXISTS (SELECT cal.id FROM ChillCalendarBundle:Calendar cal WHERE cal.calendarRange = c.id)';
if ($request->query->has('user')) {
$user = $request->query->get('user');
$sql = $sql . ' AND c.user = :user';
$query = $em->createQuery($sql)
->setParameter('user', $user);
} else {
$query = $em->createQuery($sql);
} }
$results = $query->getResult(); /**
* @Route("/api/1.0/calendar/calendar-range-available/{id}.{_format}",
* name="chill_api_single_calendar_range_available",
* requirements={"_format": "json"}
* )
*/
public function availableRanges(User $user, Request $request, string $_format): JsonResponse
{
//return new JsonResponse(['ok' => true], 200, [], false);
$this->denyAccessUnlessGranted('ROLE_USER');
return $this->json(['count' => count($results), 'results' => $results], Response::HTTP_OK, [], ['groups' => ['read']]); if (!$request->query->has('dateFrom')) {
//TODO use also the paginator, eg return $this->serializeCollection('get', $request, $_format, $paginator, $results); throw new BadRequestHttpException('You must provide a dateFrom parameter');
}
if (false === $dateFrom = DateTimeImmutable::createFromFormat(
DateTimeImmutable::ATOM,
$request->query->get('dateFrom')
)) {
throw new BadRequestHttpException('dateFrom not parsable');
}
if (!$request->query->has('dateTo')) {
throw new BadRequestHttpException('You must provide a dateTo parameter');
}
if (false === $dateTo = DateTimeImmutable::createFromFormat(
DateTimeImmutable::ATOM,
$request->query->get('dateTo')
)) {
throw new BadRequestHttpException('dateTo not parsable');
}
$total = $this->calendarRangeRepository->countByAvailableRangesForUser($user, $dateFrom, $dateTo);
$paginator = $this->getPaginatorFactory()->create($total);
$ranges = $this->calendarRangeRepository->findByAvailableRangesForUser(
$user,
$dateFrom,
$dateTo,
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
$collection = new Collection($ranges, $paginator);
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['read']]);
} }
} }

View File

@ -0,0 +1,26 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class CancelReasonController extends CRUDController
{
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
$query->addOrderBy('e.id', 'ASC');
return parent::orderQuery($action, $query, $request, $paginator);
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\Invite;
use Chill\CalendarBundle\Security\Voter\InviteVoter;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use function in_array;
class InviteApiController
{
private EntityManagerInterface $entityManager;
private Security $security;
public function __construct(Security $security, EntityManagerInterface $entityManager)
{
$this->security = $security;
$this->entityManager = $entityManager;
}
/**
* Give an answer to a calendar invite.
*
* @Route("/api/1.0/calendar/calendar/{id}/answer/{answer}.json", methods={"post"})
*/
public function answer(Calendar $calendar, string $answer): Response
{
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException('not a regular user');
}
if (null === $invite = $calendar->getInviteForUser($user)) {
throw new AccessDeniedHttpException('not invited to this calendar');
}
if (!$this->security->isGranted(InviteVoter::ANSWER, $invite)) {
throw new AccessDeniedHttpException('not allowed to answer on this invitation');
}
if (!in_array($answer, Invite::STATUSES, true) || Invite::PENDING === $answer) {
throw new BadRequestHttpException('answer not valid');
}
$invite->setStatus($answer);
$this->entityManager->flush();
return new JsonResponse(null, Response::HTTP_ACCEPTED, [], false);
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\OnBehalfOfUserTokenStorage;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use TheNetworg\OAuth2\Client\Provider\Azure;
use TheNetworg\OAuth2\Client\Token\AccessToken;
class RemoteCalendarConnectAzureController
{
private ClientRegistry $clientRegistry;
private OnBehalfOfUserTokenStorage $MSGraphTokenStorage;
public function __construct(
ClientRegistry $clientRegistry,
OnBehalfOfUserTokenStorage $MSGraphTokenStorage
) {
$this->clientRegistry = $clientRegistry;
$this->MSGraphTokenStorage = $MSGraphTokenStorage;
}
/**
* @Route("/{_locale}/connect/azure", name="chill_calendar_remote_connect_azure")
*/
public function connectAzure(Request $request): Response
{
$request->getSession()->set('azure_return_path', $request->query->get('returnPath', '/'));
return $this->clientRegistry
->getClient('azure') // key used in config/packages/knpu_oauth2_client.yaml
->redirect([
'https://graph.microsoft.com/.default', 'offline_access',
]);
}
/**
* @Route("/connect/azure/check", name="chill_calendar_remote_connect_azure_check")
*/
public function connectAzureCheck(Request $request): Response
{
/** @var Azure $client */
$client = $this->clientRegistry->getClient('azure');
try {
/** @var AccessToken $token */
$token = $client->getAccessToken();
$this->MSGraphTokenStorage->setToken($token);
} catch (IdentityProviderException $e) {
throw $e;
}
return new RedirectResponse($request->getSession()->remove('azure_return_path', '/'));
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Serializer\Model\Collection;
use DateTimeImmutable;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;
use function count;
/**
* Contains method to get events (Calendar) from remote calendar.
*/
class RemoteCalendarProxyController
{
private PaginatorFactory $paginatorFactory;
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
private SerializerInterface $serializer;
public function __construct(PaginatorFactory $paginatorFactory, RemoteCalendarConnectorInterface $remoteCalendarConnector, SerializerInterface $serializer)
{
$this->paginatorFactory = $paginatorFactory;
$this->remoteCalendarConnector = $remoteCalendarConnector;
$this->serializer = $serializer;
}
/**
* @Route("api/1.0/calendar/proxy/calendar/by-user/{id}/events")
*/
public function listEventForCalendar(User $user, Request $request): Response
{
if (!$request->query->has('dateFrom')) {
throw new BadRequestHttpException('You must provide a dateFrom parameter');
}
if (false === $dateFrom = DateTimeImmutable::createFromFormat(
DateTimeImmutable::ATOM,
$request->query->get('dateFrom')
)) {
throw new BadRequestHttpException('dateFrom not parsable');
}
if (!$request->query->has('dateTo')) {
throw new BadRequestHttpException('You must provide a dateTo parameter');
}
if (false === $dateTo = DateTimeImmutable::createFromFormat(
DateTimeImmutable::ATOM,
$request->query->get('dateTo')
)) {
throw new BadRequestHttpException('dateTo not parsable');
}
$events = $this->remoteCalendarConnector->listEventsForUser($user, $dateFrom, $dateTo);
$paginator = $this->paginatorFactory->create(count($events));
if (count($events) > 0) {
$paginator->setItemsPerPage($paginator->getTotalItems());
}
$collection = new Collection($events, $paginator);
return new JsonResponse(
$this->serializer->serialize($collection, 'json', ['groups' => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
}

View File

@ -36,6 +36,9 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
$loader->load('services/fixtures.yml'); $loader->load('services/fixtures.yml');
$loader->load('services/form.yml'); $loader->load('services/form.yml');
$loader->load('services/event.yml'); $loader->load('services/event.yml');
$loader->load('services/remote_calendar.yaml');
$container->setParameter('chill_calendar', $config);
} }
public function prepend(ContainerBuilder $container) public function prepend(ContainerBuilder $container)
@ -47,6 +50,29 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
protected function prependCruds(ContainerBuilder $container) protected function prependCruds(ContainerBuilder $container)
{ {
$container->prependExtensionConfig('chill_main', [ $container->prependExtensionConfig('chill_main', [
'cruds' => [
[
'class' => \Chill\CalendarBundle\Entity\CancelReason::class,
'name' => 'calendar_cancel-reason',
'base_path' => '/admin/calendar/cancel-reason',
'form_class' => \Chill\CalendarBundle\Form\CancelReasonType::class,
'controller' => \Chill\CalendarBundle\Controller\CancelReasonController::class,
'actions' => [
'index' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillCalendar/CancelReason/index.html.twig',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillCalendar/CancelReason/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillCalendar/CancelReason/edit.html.twig',
],
],
],
],
'apis' => [ 'apis' => [
[ [
'controller' => \Chill\CalendarBundle\Controller\CalendarRangeAPIController::class, 'controller' => \Chill\CalendarBundle\Controller\CalendarRangeAPIController::class,

View File

@ -26,9 +26,17 @@ class Configuration implements ConfigurationInterface
$treeBuilder = new TreeBuilder('chill_calendar'); $treeBuilder = new TreeBuilder('chill_calendar');
$rootNode = $treeBuilder->getRootNode('chill_calendar'); $rootNode = $treeBuilder->getRootNode('chill_calendar');
// Here you should define the parameters that are allowed to $rootNode->children()
// configure your bundle. See the documentation linked above for ->arrayNode('remote_calendars_sync')->canBeEnabled()
// more information on that topic. ->children()
->arrayNode('microsoft_graph')->canBeEnabled()
->children()
->end() // end of machine_access_token
->end() // end of microsoft_graph children
->end() // end of array microsoft_graph
->end() // end of children's remote_calendars_sync
->end() // end of array remote_calendars_sync
->end();
return $treeBuilder; return $treeBuilder;
} }

View File

@ -12,40 +12,70 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Entity; namespace Chill\CalendarBundle\Entity;
use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\Activity;
use Chill\CalendarBundle\Repository\CalendarRepository; use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable;
use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Entity\ThirdParty;
use DateInterval;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use LogicException;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Validator\Constraints\Range;
use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\ClassMetadata;
use function in_array; use function in_array;
/** /**
* @ORM\Table(name="chill_calendar.calendar") * @ORM\Table(name="chill_calendar.calendar", indexes={@ORM\Index(name="idx_calendar_remote", columns={"remoteId"})}))
* @ORM\Entity(repositoryClass=CalendarRepository::class) * @ORM\Entity
*/ */
class Calendar class Calendar implements TrackCreationInterface, TrackUpdateInterface
{ {
use RemoteCalendarTrait;
use TrackCreationTrait;
use TrackUpdateTrait;
public const STATUS_CANCELED = 'canceled'; public const STATUS_CANCELED = 'canceled';
public const STATUS_MOVED = 'moved'; public const STATUS_MOVED = 'moved';
public const STATUS_VALID = 'valid'; public const STATUS_VALID = 'valid';
/**
* a list of invite which have been added during this session.
*
* @var array|Invite[]
*/
public array $newInvites = [];
/**
* a list of invite which have been removed during this session.
*
* @var array|Invite[]
*/
public array $oldInvites = [];
public ?CalendarRange $previousCalendarRange = null;
public ?User $previousMainUser = null;
/** /**
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod") * @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod")
* @Groups({"read"}) * @Serializer\Groups({"read"})
*/ */
private AccompanyingPeriod $accompanyingPeriod; private AccompanyingPeriod $accompanyingPeriod;
@ -55,7 +85,8 @@ class Calendar
private ?Activity $activity = null; private ?Activity $activity = null;
/** /**
* @ORM\ManyToOne(targetEntity="CalendarRange", inversedBy="calendars") * @ORM\OneToOne(targetEntity="CalendarRange", inversedBy="calendar")
* @Serializer\Groups({"calendar:read", "read"})
*/ */
private ?CalendarRange $calendarRange = null; private ?CalendarRange $calendarRange = null;
@ -66,13 +97,13 @@ class Calendar
/** /**
* @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_") * @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_")
* @Serializer\Groups({"calendar:read"}) * @Serializer\Groups({"calendar:read", "read"})
*/ */
private CommentEmbeddable $comment; private CommentEmbeddable $comment;
/** /**
* @ORM\Column(type="datetimetz_immutable") * @ORM\Column(type="datetimetz_immutable")
* @Serializer\Groups({"calendar:read"}) * @Serializer\Groups({"calendar:read", "read"})
*/ */
private ?DateTimeImmutable $endDate = null; private ?DateTimeImmutable $endDate = null;
@ -80,48 +111,55 @@ class Calendar
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(type="integer") * @ORM\Column(type="integer")
* @Serializer\Groups({"calendar:read"}) * @Serializer\Groups({"calendar:read", "read"})
*/ */
private ?int $id; private ?int $id = null;
/** /**
* @ORM\ManyToMany( * @ORM\OneToMany(
* targetEntity="Invite", * targetEntity=Invite::class,
* cascade={"persist", "remove", "merge", "detach"}) * mappedBy="calendar",
* orphanRemoval=true,
* cascade={"persist", "remove", "merge", "detach"}
* )
* @ORM\JoinTable(name="chill_calendar.calendar_to_invites") * @ORM\JoinTable(name="chill_calendar.calendar_to_invites")
* @Groups({"read"}) * @Serializer\Groups({"read"})
*/ */
private Collection $invites; private Collection $invites;
/** /**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Location") * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Location")
* @groups({"read"}) * @Serializer\Groups({"read"})
*/ */
private ?Location $location = null; private ?Location $location = null;
/** /**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @Serializer\Groups({"calendar:read"}) * @Serializer\Groups({"calendar:read", "read"})
*/ */
private ?User $mainUser; private ?User $mainUser = null;
/** /**
* @ORM\ManyToMany( * @ORM\ManyToMany(
* targetEntity="Chill\PersonBundle\Entity\Person", * targetEntity="Chill\PersonBundle\Entity\Person",
* cascade={"persist", "remove", "merge", "detach"}) * cascade={"persist", "remove", "merge", "detach"})
* @ORM\JoinTable(name="chill_calendar.calendar_to_persons") * @ORM\JoinTable(name="chill_calendar.calendar_to_persons")
* @Groups({"read"}) * @Serializer\Groups({"calendar:read", "read"})
* @Serializer\Groups({"calendar:read"})
*/ */
private Collection $persons; private Collection $persons;
/**
* @ORM\Embedded(class=PrivateCommentEmbeddable::class, columnPrefix="privateComment_")
* @Serializer\Groups({"calendar:read"})
*/
private PrivateCommentEmbeddable $privateComment;
/** /**
* @ORM\ManyToMany( * @ORM\ManyToMany(
* targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty", * targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty",
* cascade={"persist", "remove", "merge", "detach"}) * cascade={"persist", "remove", "merge", "detach"})
* @ORM\JoinTable(name="chill_calendar.calendar_to_thirdparties") * @ORM\JoinTable(name="chill_calendar.calendar_to_thirdparties")
* @Groups({"read"}) * @Serializer\Groups({"calendar:read", "read"})
* @Serializer\Groups({"calendar:read"})
*/ */
private Collection $professionals; private Collection $professionals;
@ -132,52 +170,59 @@ class Calendar
/** /**
* @ORM\Column(type="datetimetz_immutable") * @ORM\Column(type="datetimetz_immutable")
* @Serializer\Groups({"calendar:read"}) * @Serializer\Groups({"calendar:read", "read"})
*/ */
private ?DateTimeImmutable $startDate = null; private ?DateTimeImmutable $startDate = null;
/** /**
* @ORM\Column(type="string", length=255) * @ORM\Column(type="string", length=255, nullable=false, options={"default": "valid"})
*/ */
private ?string $status = null; private string $status = self::STATUS_VALID;
/**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @Groups({"read"})
* @Serializer\Groups({"calendar:read"})
*/
private ?User $user = null;
public function __construct() public function __construct()
{ {
$this->comment = new CommentEmbeddable(); $this->comment = new CommentEmbeddable();
$this->privateComment = new PrivateCommentEmbeddable();
$this->persons = new ArrayCollection(); $this->persons = new ArrayCollection();
$this->professionals = new ArrayCollection(); $this->professionals = new ArrayCollection();
$this->invites = new ArrayCollection(); $this->invites = new ArrayCollection();
} }
public function addInvite(?Invite $invite): self /**
* @internal Use {@link (Calendar::addUser)} instead
*/
public function addInvite(Invite $invite): self
{ {
if (null !== $invite) { if ($invite->getCalendar() instanceof Calendar && $invite->getCalendar() !== $this) {
throw new LogicException('Not allowed to move an invitation to another Calendar');
}
$this->invites[] = $invite; $this->invites[] = $invite;
} $this->newInvites[] = $invite;
$invite->setCalendar($this);
return $this; return $this;
} }
public function addPerson(?Person $person): self public function addPerson(Person $person): self
{ {
if (null !== $person) {
$this->persons[] = $person; $this->persons[] = $person;
}
return $this; return $this;
} }
public function addProfessional(?ThirdParty $professional): self public function addProfessional(ThirdParty $professional): self
{ {
if (null !== $professional) {
$this->professionals[] = $professional; $this->professionals[] = $professional;
return $this;
}
public function addUser(User $user): self
{
if (!$this->getUsers()->contains($user) && $this->getMainUser() !== $user) {
$this->addInvite((new Invite())->setUser($user));
} }
return $this; return $this;
@ -208,6 +253,15 @@ class Calendar
return $this->comment; return $this->comment;
} }
public function getDuration(): ?DateInterval
{
if ($this->getStartDate() === null || $this->getEndDate() === null) {
return null;
}
return $this->getStartDate()->diff($this->getEndDate());
}
public function getEndDate(): ?DateTimeImmutable public function getEndDate(): ?DateTimeImmutable
{ {
return $this->endDate; return $this->endDate;
@ -218,6 +272,21 @@ class Calendar
return $this->id; return $this->id;
} }
public function getInviteForUser(User $user): ?Invite
{
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('user', $user));
$matchings = $this->invites
->matching($criteria);
if (1 === $matchings->count()) {
return $matchings->first();
}
return null;
}
/** /**
* @return Collection|Invite[] * @return Collection|Invite[]
*/ */
@ -278,6 +347,11 @@ class Calendar
return []; return [];
} }
public function getPrivateComment(): PrivateCommentEmbeddable
{
return $this->privateComment;
}
/** /**
* @return Collection|ThirdParty[] * @return Collection|ThirdParty[]
*/ */
@ -306,14 +380,30 @@ class Calendar
return $this->getProfessionals(); return $this->getProfessionals();
} }
public function getUser(): ?User /**
* @return Collection|User[]
* @Serializer\Groups({"calendar:read", "read"})
*/
public function getUsers(): Collection
{ {
return $this->user; return $this->getInvites()->map(static function (Invite $i) { return $i->getUser(); });
} }
public function getusers(): Collection public function hasCalendarRange(): bool
{ {
return $this->getInvites(); //TODO get users of the invite return null !== $this->calendarRange;
}
/**
* return true if the user is invited.
*/
public function isInvited(User $user): bool
{
if ($this->getMainUser() === $user) {
return false;
}
return $this->getUsers()->contains($user);
} }
public static function loadValidatorMetadata(ClassMetadata $metadata): void public static function loadValidatorMetadata(ClassMetadata $metadata): void
@ -330,9 +420,15 @@ class Calendar
])); ]));
} }
/**
* @internal Use {@link (Calendar::removeUser)} instead
*/
public function removeInvite(Invite $invite): self public function removeInvite(Invite $invite): self
{ {
$this->invites->removeElement($invite); if ($this->invites->removeElement($invite)) {
$invite->setCalendar(null);
$this->oldInvites[] = $invite;
}
return $this; return $this;
} }
@ -351,6 +447,20 @@ class Calendar
return $this; return $this;
} }
public function removeUser(User $user): self
{
if (!$this->getUsers()->contains($user)) {
return $this;
}
$invite = $this->invites
->filter(static function (Invite $invite) use ($user) { return $invite->getUser() === $user; })
->first();
$this->removeInvite($invite);
return $this;
}
public function setAccompanyingPeriod(?AccompanyingPeriod $accompanyingPeriod): self public function setAccompanyingPeriod(?AccompanyingPeriod $accompanyingPeriod): self
{ {
$this->accompanyingPeriod = $accompanyingPeriod; $this->accompanyingPeriod = $accompanyingPeriod;
@ -367,7 +477,16 @@ class Calendar
public function setCalendarRange(?CalendarRange $calendarRange): self public function setCalendarRange(?CalendarRange $calendarRange): self
{ {
if ($this->calendarRange !== $calendarRange) {
$this->previousCalendarRange = $this->calendarRange;
if (null !== $this->previousCalendarRange) {
$this->previousCalendarRange->setCalendar(null);
}
}
$this->calendarRange = $calendarRange; $this->calendarRange = $calendarRange;
$this->calendarRange->setCalendar($this);
return $this; return $this;
} }
@ -402,7 +521,19 @@ class Calendar
public function setMainUser(?User $mainUser): self public function setMainUser(?User $mainUser): self
{ {
if ($this->mainUser !== $mainUser) {
$this->previousMainUser = $this->mainUser;
}
$this->mainUser = $mainUser; $this->mainUser = $mainUser;
$this->removeUser($mainUser);
return $this;
}
public function setPrivateComment(PrivateCommentEmbeddable $privateComment): self
{
$this->privateComment = $privateComment;
return $this; return $this;
} }
@ -427,11 +558,4 @@ class Calendar
return $this; return $this;
} }
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
} }

View File

@ -11,29 +11,35 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Entity; namespace Chill\CalendarBundle\Entity;
use Chill\CalendarBundle\Repository\CalendarRangeRepository; use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
/** /**
* @ORM\Table(name="chill_calendar.calendar_range") * @ORM\Table(name="chill_calendar.calendar_range", indexes={@ORM\Index(name="idx_calendar_range_remote", columns={"remoteId"})})
* @ORM\Entity(repositoryClass=CalendarRangeRepository::class) * @ORM\Entity
*/ */
class CalendarRange class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
{ {
use RemoteCalendarTrait;
use TrackCreationTrait;
use TrackUpdateTrait;
/** /**
* @ORM\OneToMany(targetEntity=Calendar::class, * @ORM\OneToOne(targetEntity=Calendar::class, mappedBy="calendarRange")
* mappedBy="calendarRange")
*/ */
private Collection $calendars; private ?Calendar $calendar = null;
/** /**
* @ORM\Column(type="datetimetz_immutable") * @ORM\Column(type="datetimetz_immutable")
* @groups({"read", "write"}) * @groups({"read", "write", "calendar:read"})
*/ */
private ?DateTimeImmutable $endDate = null; private ?DateTimeImmutable $endDate = null;
@ -47,23 +53,23 @@ class CalendarRange
/** /**
* @ORM\Column(type="datetimetz_immutable") * @ORM\Column(type="datetimetz_immutable")
* @groups({"read", "write"}) * @groups({"read", "write", "calendar:read"})
*/ */
private ?DateTimeImmutable $startDate = null; private ?DateTimeImmutable $startDate = null;
/** /**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @groups({"read", "write"}) * @groups({"read", "write", "calendar:read"})
*/ */
private ?User $user = null; private ?User $user = null;
//TODO Lieu public function getCalendar(): Calendar
public function __construct()
{ {
$this->calendars = new ArrayCollection(); return $this->calendar;
} }
//TODO Lieu
public function getEndDate(): ?DateTimeImmutable public function getEndDate(): ?DateTimeImmutable
{ {
return $this->endDate; return $this->endDate;
@ -84,6 +90,14 @@ class CalendarRange
return $this->user; return $this->user;
} }
/**
* @internal use {@link (Calendar::setCalendarRange)} instead
*/
public function setCalendar(?Calendar $calendar): void
{
$this->calendar = $calendar;
}
public function setEndDate(DateTimeImmutable $endDate): self public function setEndDate(DateTimeImmutable $endDate): self
{ {
$this->endDate = $endDate; $this->endDate = $endDate;

View File

@ -11,39 +11,80 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Entity; namespace Chill\CalendarBundle\Entity;
use Chill\CalendarBundle\Repository\InviteRepository; use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use LogicException;
use Symfony\Component\Serializer\Annotation as Serializer;
/** /**
* @ORM\Table(name="chill_calendar.invite") * @ORM\Table(name="chill_calendar.invite")
* @ORM\Entity(repositoryClass=InviteRepository::class) * @ORM\Entity
*/ */
class Invite class Invite implements TrackUpdateInterface, TrackCreationInterface
{ {
use TrackCreationTrait;
use TrackUpdateTrait;
public const ACCEPTED = 'accepted';
public const DECLINED = 'declined';
public const PENDING = 'pending';
/**
* all statuses in one const.
*/
public const STATUSES = [
self::ACCEPTED,
self::DECLINED,
self::PENDING,
self::TENTATIVELY_ACCEPTED,
];
public const TENTATIVELY_ACCEPTED = 'tentative';
/**
* @ORM\ManyToOne(targetEntity=Calendar::class, inversedBy="invites")
*/
private ?Calendar $calendar = null;
/** /**
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(type="integer") * @ORM\Column(type="integer")
* @Serializer\Groups(groups={"calendar:read", "read"})
*/ */
private $id; private ?int $id = null;
/** /**
* @ORM\Column(type="json") * @ORM\Column(type="text", nullable=false, options={"default": "pending"})
* @Serializer\Groups(groups={"calendar:read", "read"})
*/ */
private array $status = []; private string $status = self::PENDING;
/** /**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @ORM\JoinColumn(nullable=false)
* @Serializer\Groups(groups={"calendar:read", "read"})
*/ */
private User $user; private ?User $user = null;
public function getCalendar(): ?Calendar
{
return $this->calendar;
}
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
} }
public function getStatus(): ?array public function getStatus(): string
{ {
return $this->status; return $this->status;
} }
@ -53,7 +94,15 @@ class Invite
return $this->user; return $this->user;
} }
public function setStatus(array $status): self /**
* @internal use Calendar::addInvite instead
*/
public function setCalendar(?Calendar $calendar): void
{
$this->calendar = $calendar;
}
public function setStatus(string $status): self
{ {
$this->status = $status; $this->status = $status;
@ -62,6 +111,10 @@ class Invite
public function setUser(?User $user): self public function setUser(?User $user): self
{ {
if ($user instanceof User && $this->user instanceof User && $user !== $this->user) {
throw new LogicException('Not allowed to associate an invite to a different user');
}
$this->user = $user; $this->user = $user;
return $this; return $this;

View File

@ -0,0 +1,64 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
trait RemoteCalendarTrait
{
/**
* If true, the changes won't be enqueued to remote.
*
* This is required to prevent update loop: a persist trigger an event creation on remote,
* which in turn change remoteId and, in turn, trigger an update change, ...
*/
public bool $preventEnqueueChanges = false;
/**
* @ORM\Column(type="json", options={"default": "[]"}, nullable=false)
*/
private array $remoteAttributes = [];
/**
* @ORM\Column(type="text", options={"default": ""}, nullable=false)
*/
private string $remoteId = '';
public function addRemoteAttributes(array $remoteAttributes): self
{
$this->remoteAttributes = array_merge($this->remoteAttributes, $remoteAttributes);
return $this;
}
public function getRemoteAttributes(): array
{
return $this->remoteAttributes;
}
public function getRemoteId(): string
{
return $this->remoteId;
}
public function hasRemoteId(): bool
{
return '' !== $this->remoteId;
}
public function setRemoteId(string $remoteId): self
{
$this->remoteId = $remoteId;
return $this;
}
}

View File

@ -12,17 +12,19 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Form; namespace Chill\CalendarBundle\Form;
use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Entity\CancelReason; use Chill\CalendarBundle\Entity\CancelReason;
use Chill\CalendarBundle\Entity\Invite; use Chill\CalendarBundle\Form\DataTransformer\IdToCalendarRangeDataTransformer;
use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Form\DataTransformer\IdToLocationDataTransformer;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Form\DataTransformer\IdToUserDataTransformer;
use Chill\MainBundle\Form\DataTransformer\IdToUsersDataTransformer;
use Chill\MainBundle\Form\Type\CommentType; use Chill\MainBundle\Form\Type\CommentType;
use Chill\PersonBundle\Form\DataTransformer\PersonsToIdDataTransformer;
use Chill\ThirdPartyBundle\Form\DataTransformer\ThirdPartiesToIdDataTransformer;
use Chill\MainBundle\Form\Type\PrivateCommentType;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Entity\ThirdParty;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Persistence\ObjectManager;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\CallbackTransformer;
@ -33,16 +35,32 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class CalendarType extends AbstractType class CalendarType extends AbstractType
{ {
protected ObjectManager $om; private IdToCalendarRangeDataTransformer $calendarRangeDataTransformer;
protected TranslatableStringHelper $translatableStringHelper; private IdToLocationDataTransformer $idToLocationDataTransformer;
private IdToUserDataTransformer $idToUserDataTransformer;
private IdToUsersDataTransformer $idToUsersDataTransformer;
private ThirdPartiesToIdDataTransformer $partiesToIdDataTransformer;
private PersonsToIdDataTransformer $personsToIdDataTransformer;
public function __construct( public function __construct(
TranslatableStringHelper $translatableStringHelper, PersonsToIdDataTransformer $personsToIdDataTransformer,
ObjectManager $om IdToUserDataTransformer $idToUserDataTransformer,
IdToUsersDataTransformer $idToUsersDataTransformer,
IdToLocationDataTransformer $idToLocationDataTransformer,
ThirdPartiesToIdDataTransformer $partiesToIdDataTransformer,
IdToCalendarRangeDataTransformer $idToCalendarRangeDataTransformer
) { ) {
$this->translatableStringHelper = $translatableStringHelper; $this->personsToIdDataTransformer = $personsToIdDataTransformer;
$this->om = $om; $this->idToUserDataTransformer = $idToUserDataTransformer;
$this->idToUsersDataTransformer = $idToUsersDataTransformer;
$this->idToLocationDataTransformer = $idToLocationDataTransformer;
$this->partiesToIdDataTransformer = $partiesToIdDataTransformer;
$this->calendarRangeDataTransformer = $idToCalendarRangeDataTransformer;
} }
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
@ -51,6 +69,10 @@ class CalendarType extends AbstractType
->add('comment', CommentType::class, [ ->add('comment', CommentType::class, [
'required' => false, 'required' => false,
]) ])
->add('privateComment', PrivateCommentType::class, [
'required' => false,
'label' => 'private comment',
])
// ->add('cancelReason', EntityType::class, [ // ->add('cancelReason', EntityType::class, [
// 'required' => false, // 'required' => false,
// 'class' => CancelReason::class, // 'class' => CancelReason::class,
@ -68,36 +90,26 @@ class CalendarType extends AbstractType
]); ]);
$builder->add('mainUser', HiddenType::class); $builder->add('mainUser', HiddenType::class);
$builder->get('mainUser') $builder->get('mainUser')->addModelTransformer($this->idToUserDataTransformer);
->addModelTransformer(new CallbackTransformer(
static function (?User $user): int {
if (null !== $user) {
$res = $user->getId();
} else {
$res = -1; //TODO cannot be null in any ways...
}
return $res;
},
function (?int $userId): User {
return $this->om->getRepository(user::class)->findOneBy(['id' => (int) $userId]);
}
));
$builder->add('startDate', HiddenType::class); $builder->add('startDate', HiddenType::class);
$builder->get('startDate') $builder->get('startDate')
->addModelTransformer(new CallbackTransformer( ->addModelTransformer(new CallbackTransformer(
static function (?DateTimeImmutable $dateTimeImmutable): string { static function (?DateTimeImmutable $dateTimeImmutable): string {
if (null !== $dateTimeImmutable) { if (null !== $dateTimeImmutable) {
$res = date_format($dateTimeImmutable, 'Y-m-d H:i:s'); $res = date_format($dateTimeImmutable, DateTimeImmutable::ATOM);
} else { } else {
$res = ''; $res = '';
} }
return $res; return $res;
}, },
static function (?string $dateAsString): DateTimeImmutable { static function (?string $dateAsString): ?DateTimeImmutable {
return new DateTimeImmutable($dateAsString); if ('' === $dateAsString) {
return null;
}
return DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, $dateAsString);
} }
)); ));
@ -106,121 +118,41 @@ class CalendarType extends AbstractType
->addModelTransformer(new CallbackTransformer( ->addModelTransformer(new CallbackTransformer(
static function (?DateTimeImmutable $dateTimeImmutable): string { static function (?DateTimeImmutable $dateTimeImmutable): string {
if (null !== $dateTimeImmutable) { if (null !== $dateTimeImmutable) {
$res = date_format($dateTimeImmutable, 'Y-m-d H:i:s'); $res = date_format($dateTimeImmutable, DateTimeImmutable::ATOM);
} else { } else {
$res = ''; $res = '';
} }
return $res; return $res;
}, },
static function (?string $dateAsString): DateTimeImmutable { static function (?string $dateAsString): ?DateTimeImmutable {
return new DateTimeImmutable($dateAsString); if ('' === $dateAsString) {
return null;
}
return DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, $dateAsString);
} }
)); ));
$builder->add('persons', HiddenType::class); $builder->add('persons', HiddenType::class);
$builder->get('persons') $builder->get('persons')
->addModelTransformer(new CallbackTransformer( ->addModelTransformer($this->personsToIdDataTransformer);
static function (iterable $personsAsIterable): string {
$personIds = [];
foreach ($personsAsIterable as $value) {
$personIds[] = $value->getId();
}
return implode(',', $personIds);
},
function (?string $personsAsString): array {
if (null === $personsAsString) {
return [];
}
return array_map(
fn (string $id): ?Person => $this->om->getRepository(Person::class)->findOneBy(['id' => (int) $id]),
explode(',', $personsAsString)
);
}
));
$builder->add('professionals', HiddenType::class); $builder->add('professionals', HiddenType::class);
$builder->get('professionals') $builder->get('professionals')
->addModelTransformer(new CallbackTransformer( ->addModelTransformer($this->partiesToIdDataTransformer);
static function (iterable $thirdpartyAsIterable): string {
$thirdpartyIds = [];
foreach ($thirdpartyAsIterable as $value) { $builder->add('users', HiddenType::class);
$thirdpartyIds[] = $value->getId(); $builder->get('users')
} ->addModelTransformer($this->idToUsersDataTransformer);
return implode(',', $thirdpartyIds);
},
function (?string $thirdpartyAsString): array {
if (null === $thirdpartyAsString) {
return [];
}
return array_map(
fn (string $id): ?ThirdParty => $this->om->getRepository(ThirdParty::class)->findOneBy(['id' => (int) $id]),
explode(',', $thirdpartyAsString)
);
}
));
$builder->add('calendarRange', HiddenType::class); $builder->add('calendarRange', HiddenType::class);
$builder->get('calendarRange') $builder->get('calendarRange')
->addModelTransformer(new CallbackTransformer( ->addModelTransformer($this->calendarRangeDataTransformer);
static function (?CalendarRange $calendarRange): ?int {
if (null !== $calendarRange) {
$res = $calendarRange->getId();
} else {
$res = -1;
}
return $res;
},
function (?string $calendarRangeId): ?CalendarRange {
if (null !== $calendarRangeId) {
$res = $this->om->getRepository(CalendarRange::class)->findOneBy(['id' => (int) $calendarRangeId]);
} else {
$res = null;
}
return $res;
}
));
$builder->add('location', HiddenType::class) $builder->add('location', HiddenType::class)
->get('location') ->get('location')
->addModelTransformer(new CallbackTransformer( ->addModelTransformer($this->idToLocationDataTransformer);
static function (?Location $location): string {
if (null === $location) {
return '';
}
return $location->getId();
},
function (?string $id): ?Location {
return $this->om->getRepository(Location::class)->findOneBy(['id' => (int) $id]);
}
));
$builder->add('invites', HiddenType::class);
$builder->get('invites')
->addModelTransformer(new CallbackTransformer(
static function (iterable $usersAsIterable): string {
$userIds = [];
foreach ($usersAsIterable as $value) {
$userIds[] = $value->getId();
}
return implode(',', $userIds);
},
function (?string $usersAsString): array {
return array_map(
fn (string $id): ?Invite => $this->om->getRepository(Invite::class)->findOneBy(['id' => (int) $id]),
explode(',', $usersAsString)
);
}
));
} }
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
@ -228,10 +160,6 @@ class CalendarType extends AbstractType
$resolver->setDefaults([ $resolver->setDefaults([
'data_class' => Calendar::class, 'data_class' => Calendar::class,
]); ]);
$resolver
->setRequired(['accompanyingPeriod'])
->setAllowedTypes('accompanyingPeriod', [\Chill\PersonBundle\Entity\AccompanyingPeriod::class, 'null']);
} }
public function getBlockPrefix() public function getBlockPrefix()

View File

@ -0,0 +1,39 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Form;
use Chill\CalendarBundle\Entity\CancelReason;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CancelReasonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TranslatableStringFormType::class)
->add('active', CheckboxType::class, [
'required' => false,
])
->add('canceledBy', TextType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('class', CancelReason::class);
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Form\DataTransformer;
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
use Chill\MainBundle\Form\DataTransformer\IdToEntityDataTransformer;
class IdToCalendarRangeDataTransformer extends IdToEntityDataTransformer
{
public function __construct(CalendarRangeRepository $repository)
{
parent::__construct($repository, false);
}
}

View File

@ -13,20 +13,16 @@ namespace Chill\CalendarBundle\Menu;
use Chill\CalendarBundle\Security\Voter\CalendarVoter; use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
{ {
private Security $security;
protected TranslatorInterface $translator; protected TranslatorInterface $translator;
private Security $security;
public function __construct( public function __construct(
Security $security, Security $security,
TranslatorInterface $translator TranslatorInterface $translator

View File

@ -0,0 +1,54 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class AdminMenuBuilder implements LocalMenuBuilderInterface
{
/**
* @var AuthorizationCheckerInterface
*/
protected $authorizationChecker;
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
return;
}
$menu->addChild('Calendar', [
'route' => 'chill_calendar_admin_index',
])
->setAttribute('class', 'list-group-item-header')
->setExtras([
'order' => 6000,
'icons' => ['calendar'],
]);
$menu->addChild('Cancel reason', [
'route' => 'chill_crud_calendar_cancel-reason_index',
])->setExtras(['order' => 6010]);
}
public static function getMenuIds(): array
{
return ['admin_section', 'admin_calendar'];
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Messenger\Doctrine;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\Security;
class CalendarEntityListener
{
private MessageBusInterface $messageBus;
private Security $security;
public function __construct(MessageBusInterface $messageBus, Security $security)
{
$this->messageBus = $messageBus;
$this->security = $security;
}
public function postPersist(Calendar $calendar, LifecycleEventArgs $args): void
{
if (!$calendar->preventEnqueueChanges) {
$this->messageBus->dispatch(
new CalendarMessage(
$calendar,
CalendarMessage::CALENDAR_PERSIST,
$this->security->getUser()
)
);
}
}
public function postRemove(Calendar $calendar, LifecycleEventArgs $args): void
{
// TODO
}
public function postUpdate(Calendar $calendar, LifecycleEventArgs $args): void
{
if (!$calendar->preventEnqueueChanges) {
$this->messageBus->dispatch(
new CalendarMessage(
$calendar,
CalendarMessage::CALENDAR_UPDATE,
$this->security->getUser()
)
);
}
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Messenger\Doctrine;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Messenger\Message\CalendarRangeMessage;
use Chill\CalendarBundle\Messenger\Message\CalendarRangeRemovedMessage;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\Security;
class CalendarRangeEntityListener
{
private MessageBusInterface $messageBus;
private Security $security;
public function __construct(MessageBusInterface $messageBus, Security $security)
{
$this->messageBus = $messageBus;
$this->security = $security;
}
public function postPersist(CalendarRange $calendarRange, LifecycleEventArgs $eventArgs): void
{
if (!$calendarRange->preventEnqueueChanges) {
$this->messageBus->dispatch(
new CalendarRangeMessage(
$calendarRange,
CalendarRangeMessage::CALENDAR_RANGE_PERSIST,
$this->security->getUser()
)
);
}
}
public function postRemove(CalendarRange $calendarRange, LifecycleEventArgs $eventArgs): void
{
if (!$calendarRange->preventEnqueueChanges) {
$this->messageBus->dispatch(
new CalendarRangeRemovedMessage(
$calendarRange,
$this->security->getUser()
)
);
}
}
public function postUpdate(CalendarRange $calendarRange, LifecycleEventArgs $eventArgs): void
{
if (!$calendarRange->preventEnqueueChanges) {
$this->messageBus->dispatch(
new CalendarRangeMessage(
$calendarRange,
CalendarRangeMessage::CALENDAR_RANGE_UPDATE,
$this->security->getUser()
)
);
}
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Messenger\Handler;
use Chill\CalendarBundle\Messenger\Message\CalendarRangeRemovedMessage;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Chill\MainBundle\Repository\UserRepository;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
/**
* @AsMessageHandler
*/
class CalendarRangeRemoveToRemoteHandler implements MessageHandlerInterface
{
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
private UserRepository $userRepository;
public function __construct(RemoteCalendarConnectorInterface $remoteCalendarConnector, UserRepository $userRepository)
{
$this->remoteCalendarConnector = $remoteCalendarConnector;
$this->userRepository = $userRepository;
}
public function __invoke(CalendarRangeRemovedMessage $calendarRangeRemovedMessage)
{
$this->remoteCalendarConnector->removeCalendarRange(
$calendarRangeRemovedMessage->getRemoteId(),
$calendarRangeRemovedMessage->getRemoteAttributes(),
$this->userRepository->find($calendarRangeRemovedMessage->getCalendarRangeUserId())
);
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Messenger\Handler;
use Chill\CalendarBundle\Messenger\Message\CalendarRangeMessage;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
/**
* @AsMessageHandler
*/
class CalendarRangeToRemoteHandler implements MessageHandlerInterface
{
private CalendarRangeRepository $calendarRangeRepository;
private EntityManagerInterface $entityManager;
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
public function __construct(
CalendarRangeRepository $calendarRangeRepository,
RemoteCalendarConnectorInterface $remoteCalendarConnector,
EntityManagerInterface $entityManager
) {
$this->calendarRangeRepository = $calendarRangeRepository;
$this->remoteCalendarConnector = $remoteCalendarConnector;
$this->entityManager = $entityManager;
}
public function __invoke(CalendarRangeMessage $calendarRangeMessage): void
{
$range = $this->calendarRangeRepository->find($calendarRangeMessage->getCalendarRangeId());
$this->remoteCalendarConnector->syncCalendarRange($range);
$range->preventEnqueueChanges = true;
$this->entityManager->flush();
}
}

View File

@ -0,0 +1,106 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Messenger\Handler;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Entity\Invite;
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
use Chill\CalendarBundle\Repository\CalendarRepository;
use Chill\CalendarBundle\Repository\InviteRepository;
use Chill\MainBundle\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
/**
* @AsMessageHandler
*/
class CalendarToRemoteHandler implements MessageHandlerInterface
{
private RemoteCalendarConnectorInterface $calendarConnector;
private CalendarRangeRepository $calendarRangeRepository;
private CalendarRepository $calendarRepository;
private EntityManagerInterface $entityManager;
private InviteRepository $inviteRepository;
private UserRepository $userRepository;
public function __construct(
CalendarRangeRepository $calendarRangeRepository,
CalendarRepository $calendarRepository,
EntityManagerInterface $entityManager,
InviteRepository $inviteRepository,
RemoteCalendarConnectorInterface $calendarConnector,
UserRepository $userRepository
) {
$this->calendarConnector = $calendarConnector;
$this->calendarRepository = $calendarRepository;
$this->calendarRangeRepository = $calendarRangeRepository;
$this->entityManager = $entityManager;
$this->userRepository = $userRepository;
$this->inviteRepository = $inviteRepository;
}
public function __invoke(CalendarMessage $calendarMessage)
{
$calendar = $this->calendarRepository->find($calendarMessage->getCalendarId());
if (null !== $calendarMessage->getPreviousCalendarRangeId()) {
$previousCalendarRange = $this->calendarRangeRepository
->find($calendarMessage->getPreviousCalendarRangeId());
} else {
$previousCalendarRange = null;
}
if (null !== $calendarMessage->getPreviousMainUserId()) {
$previousMainUser = $this->userRepository
->find($calendarMessage->getPreviousMainUserId());
} else {
$previousMainUser = null;
}
$newInvites = array_filter(
array_map(
function ($id) { return $this->inviteRepository->find($id); },
$calendarMessage->getNewInvitesIds(),
),
static function (?Invite $invite) { return null !== $invite; }
);
$this->calendarConnector->syncCalendar(
$calendar,
$calendarMessage->getAction(),
$previousCalendarRange,
$previousMainUser,
$calendarMessage->getOldInvites(),
$newInvites
);
$calendar->preventEnqueueChanges = true;
if ($calendar->hasCalendarRange()) {
$calendar->getCalendarRange()->preventEnqueueChanges = true;
}
if ($previousCalendarRange instanceof CalendarRange) {
$previousCalendarRange->preventEnqueueChanges = true;
}
$this->entityManager->flush();
}
}

View File

@ -0,0 +1,104 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Messenger\Message;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\Invite;
use Chill\MainBundle\Entity\User;
class CalendarMessage
{
public const CALENDAR_PERSIST = 'CHILL_CALENDAR_CALENDAR_PERSIST';
public const CALENDAR_UPDATE = 'CHILL_CALENDAR_CALENDAR_UPDATE';
private string $action;
private int $byUserId;
private int $calendarId;
private array $newInvitesIds = [];
/**
* @var array<array{inviteId: int, userId: int, userEmail: int, userLabel: string}>
*/
private array $oldInvites = [];
private ?int $previousCalendarRangeId = null;
private ?int $previousMainUserId = null;
public function __construct(
Calendar $calendar,
string $action,
User $byUser
) {
$this->calendarId = $calendar->getId();
$this->byUserId = $byUser->getId();
$this->action = $action;
$this->previousCalendarRangeId = null !== $calendar->previousCalendarRange ?
$calendar->previousCalendarRange->getId() : null;
$this->previousMainUserId = null !== $calendar->previousMainUser ?
$calendar->previousMainUser->getId() : null;
$this->newInvitesIds = array_map(static fn (Invite $i) => $i->getId(), $calendar->newInvites);
$this->oldInvites = array_map(static function (Invite $i) {
return [
'inviteId' => $i->getId(),
'userId' => $i->getUser()->getId(),
'userEmail' => $i->getUser()->getEmail(),
'userLabel' => $i->getUser()->getLabel(),
];
}, $calendar->oldInvites);
}
public function getAction(): string
{
return $this->action;
}
public function getByUserId(): ?int
{
return $this->byUserId;
}
public function getCalendarId(): ?int
{
return $this->calendarId;
}
/**
* @return array|int[]|null[]
*/
public function getNewInvitesIds(): array
{
return $this->newInvitesIds;
}
/**
* @return array<array{inviteId: int, userId: int, userEmail: int, userLabel: string}>
*/
public function getOldInvites(): array
{
return $this->oldInvites;
}
public function getPreviousCalendarRangeId(): ?int
{
return $this->previousCalendarRangeId;
}
public function getPreviousMainUserId(): ?int
{
return $this->previousMainUserId;
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Messenger\Message;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\MainBundle\Entity\User;
class CalendarRangeMessage
{
public const CALENDAR_RANGE_PERSIST = 'CHILL_CALENDAR_CALENDAR_RANGE_PERSIST';
public const CALENDAR_RANGE_UPDATE = 'CHILL_CALENDAR_CALENDAR_RANGE_UPDATE';
private string $action;
private ?int $byUserId = null;
private int $calendarRangeId;
public function __construct(CalendarRange $calendarRange, string $action, ?User $byUser)
{
$this->action = $action;
$this->calendarRangeId = $calendarRange->getId();
if (null !== $byUser) {
$this->byUserId = $byUser->getId();
}
}
public function getAction(): string
{
return $this->action;
}
public function getByUserId(): ?int
{
return $this->byUserId;
}
public function getCalendarRangeId(): ?int
{
return $this->calendarRangeId;
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Messenger\Message;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\MainBundle\Entity\User;
class CalendarRangeRemovedMessage
{
private ?int $byUserId = null;
private int $calendarRangeUserId;
private array $remoteAttributes;
private string $remoteId;
public function __construct(CalendarRange $calendarRange, ?User $byUser)
{
$this->remoteId = $calendarRange->getRemoteId();
$this->remoteAttributes = $calendarRange->getRemoteAttributes();
$this->calendarRangeUserId = $calendarRange->getUser()->getId();
if (null !== $byUser) {
$this->byUserId = $byUser->getId();
}
}
public function getByUserId(): ?int
{
return $this->byUserId;
}
public function getCalendarRangeUserId(): ?int
{
return $this->calendarRangeUserId;
}
public function getRemoteAttributes(): array
{
return $this->remoteAttributes;
}
public function getRemoteId(): string
{
return $this->remoteId;
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use LogicException;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
class MachineHttpClient implements HttpClientInterface
{
use BearerAuthorizationTrait;
private HttpClientInterface $decoratedClient;
private MachineTokenStorage $machineTokenStorage;
/**
* @param HttpClientInterface $decoratedClient
*/
public function __construct(MachineTokenStorage $machineTokenStorage, ?HttpClientInterface $decoratedClient = null)
{
$this->decoratedClient = $decoratedClient ?? \Symfony\Component\HttpClient\HttpClient::create();
$this->machineTokenStorage = $machineTokenStorage;
}
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$options['headers'] = array_merge(
$options['headers'] ?? [],
$this->getAuthorizationHeaders($this->machineTokenStorage->getToken())
);
$options['base_uri'] = 'https://graph.microsoft.com/v1.0/';
switch ($method) {
case 'GET':
case 'HEAD':
case 'DELETE':
$options['headers']['Accept'] = 'application/json';
break;
case 'POST':
case 'PUT':
case 'PATCH':
$options['headers']['Content-Type'] = 'application/json';
break;
default:
throw new LogicException("Method not supported: {$method}");
}
return $this->decoratedClient->request($method, $url, $options);
}
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
return $this->decoratedClient->stream($responses, $timeout);
}
}

View File

@ -0,0 +1,50 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
use Chill\MainBundle\Redis\ChillRedis;
use League\OAuth2\Client\Token\AccessTokenInterface;
use TheNetworg\OAuth2\Client\Provider\Azure;
use TheNetworg\OAuth2\Client\Token\AccessToken;
class MachineTokenStorage
{
private const KEY = 'msgraph_access_token';
private ?AccessTokenInterface $accessToken = null;
private Azure $azure;
private ChillRedis $chillRedis;
public function __construct(Azure $azure, ChillRedis $chillRedis)
{
$this->azure = $azure;
$this->chillRedis = $chillRedis;
}
public function getToken(): AccessTokenInterface
{
if (null === $this->accessToken || $this->accessToken->hasExpired()) {
$this->accessToken = $this->azure->getAccessToken('client_credentials', [
'scope' => 'https://graph.microsoft.com/.default',
]);
}
return $this->accessToken;
}
public function storeToken(AccessToken $token): void
{
$this->chillRedis->set(self::KEY, serialize($token));
}
}

View File

@ -0,0 +1,97 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
use Chill\MainBundle\Entity\User;
use Psr\Log\LoggerInterface;
/**
* Write metadata to user, which allow to find his default calendar.
*/
class MapCalendarToUser
{
public const METADATA_KEY = 'msgraph';
private LoggerInterface $logger;
private MachineHttpClient $machineHttpClient;
public function __construct(
MachineHttpClient $machineHttpClient,
LoggerInterface $logger
) {
$this->machineHttpClient = $machineHttpClient;
$this->logger = $logger;
}
public function getCalendarId(User $user): ?string
{
if (null === $msKey = ($user->getAttributes()[self::METADATA_KEY] ?? null)) {
return null;
}
return $msKey['defaultCalendarId'] ?? null;
}
public function getDefaultUserCalendar(string $idOrUserPrincipalName): ?array
{
$value = $this->machineHttpClient->request('GET', "users/{$idOrUserPrincipalName}/calendars", [
'query' => ['$filter' => 'isDefaultCalendar eq true'],
])->toArray()['value'];
return $value[0] ?? null;
}
public function getUserByEmail(string $email): ?array
{
$value = $this->machineHttpClient->request('GET', 'users', [
'query' => ['$filter' => "mail eq '{$email}'"],
])->toArray()['value'];
return $value[0] ?? null;
}
public function getUserId(User $user): ?string
{
if (null === $msKey = ($user->getAttributes()[self::METADATA_KEY] ?? null)) {
return null;
}
return $msKey['id'] ?? null;
}
public function writeMetadata(User $user): User
{
if (null === $userData = $this->getUserByEmail($user->getEmailCanonical())) {
$this->logger->warning('[MapCalendarToUser] could find user on msgraph', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]);
return $this->writeNullData($user);
}
if (null === $defaultCalendar = $this->getDefaultUserCalendar($userData['id'])) {
$this->logger->warning('[MapCalendarToUser] could find default calendar', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]);
return $this->writeNullData($user);
}
return $user->setAttributes([self::METADATA_KEY => [
'id' => $userData['id'],
'userPrincipalName' => $userData['userPrincipalName'],
'defaultCalendarId' => $defaultCalendar['id'],
]]);
}
private function writeNullData(User $user): User
{
return $user->unsetAttribute(self::METADATA_KEY);
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use LogicException;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
class OnBehalfOfUserHttpClient
{
use BearerAuthorizationTrait;
private HttpClientInterface $decoratedClient;
private OnBehalfOfUserTokenStorage $tokenStorage;
/**
* @param HttpClientInterface $decoratedClient
*/
public function __construct(OnBehalfOfUserTokenStorage $tokenStorage, ?HttpClientInterface $decoratedClient = null)
{
$this->decoratedClient = $decoratedClient ?? \Symfony\Component\HttpClient\HttpClient::create();
$this->tokenStorage = $tokenStorage;
}
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$options['headers'] = array_merge(
$options['headers'] ?? [],
$this->getAuthorizationHeaders($this->tokenStorage->getToken())
);
$options['base_uri'] = 'https://graph.microsoft.com/v1.0/';
switch ($method) {
case 'GET':
case 'HEAD':
$options['headers']['Accept'] = 'application/json';
break;
case 'POST':
case 'PUT':
case 'PATCH':
$options['headers']['Content-Type'] = 'application/json';
break;
default:
throw new LogicException("Method not supported: {$method}");
}
return $this->decoratedClient->request($method, $url, $options);
}
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
return $this->decoratedClient->stream($responses, $timeout);
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
use LogicException;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use TheNetworg\OAuth2\Client\Provider\Azure;
use TheNetworg\OAuth2\Client\Token\AccessToken;
/**
* Store token obtained on behalf of a User.
*/
class OnBehalfOfUserTokenStorage
{
public const MS_GRAPH_ACCESS_TOKEN = 'msgraph_access_token';
private Azure $azure;
private SessionInterface $session;
public function __construct(Azure $azure, SessionInterface $session)
{
$this->azure = $azure;
$this->session = $session;
}
public function getToken(): AccessToken
{
/** @var ?AccessToken $token */
$token = $this->session->get(self::MS_GRAPH_ACCESS_TOKEN, null);
if (null === $token) {
throw new LogicException('unexisting token');
}
if ($token->hasExpired()) {
$token = $this->azure->getAccessToken('refresh_token', [
'refresh_token' => $token->getRefreshToken(),
]);
$this->setToken($token);
}
return $token;
}
public function hasToken(): bool
{
return $this->session->has(self::MS_GRAPH_ACCESS_TOKEN);
}
public function setToken(AccessToken $token): void
{
$this->session->set(self::MS_GRAPH_ACCESS_TOKEN, $token);
}
}

View File

@ -0,0 +1,195 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Entity\Invite;
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use DateTimeImmutable;
use DateTimeZone;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Convert Chill Calendar event to Remote MS Graph event, and MS Graph
* event to RemoteEvent.
*/
class RemoteEventConverter
{
public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P';
private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';
private DateTimeZone $defaultDateTimeZone;
private EngineInterface $engine;
private PersonRenderInterface $personRender;
private DateTimeZone $remoteDateTimeZone;
private TranslatorInterface $translator;
public function __construct(EngineInterface $engine, PersonRenderInterface $personRender, TranslatorInterface $translator)
{
$this->engine = $engine;
$this->translator = $translator;
$this->personRender = $personRender;
$this->defaultDateTimeZone = (new DateTimeImmutable())->getTimezone();
$this->remoteDateTimeZone = self::getRemoteTimeZone();
}
/**
* Transform a CalendarRange into a representation suitable for storing into MSGraph.
*
* @return array an array representation for event in MS Graph
*/
public function calendarRangeToEvent(CalendarRange $calendarRange): array
{
return [
'subject' => $this->translator->trans('remote_calendar.calendar_range_title'),
'start' => [
'dateTime' => $calendarRange->getStartDate()->setTimezone($this->remoteDateTimeZone)
->format(self::REMOTE_DATE_FORMAT),
'timeZone' => 'UTC',
],
'end' => [
'dateTime' => $calendarRange->getEndDate()->setTimezone($this->remoteDateTimeZone)
->format(self::REMOTE_DATE_FORMAT),
'timeZone' => 'UTC',
],
'attendees' => [
[
'emailAddress' => [
'address' => $calendarRange->getUser()->getEmailCanonical(),
'name' => $calendarRange->getUser()->getLabel(),
],
],
],
'isReminderOn' => false,
];
}
public function calendarToEvent(Calendar $calendar): array
{
return array_merge(
[
'subject' => '[Chill] ' .
implode(
', ',
$calendar->getPersons()->map(function (Person $p) {
return $this->personRender->renderString($p, []);
})->toArray()
),
'start' => [
'dateTime' => $calendar->getStartDate()->setTimezone($this->remoteDateTimeZone)
->format(self::REMOTE_DATE_FORMAT),
'timeZone' => 'UTC',
],
'end' => [
'dateTime' => $calendar->getEndDate()->setTimezone($this->remoteDateTimeZone)
->format(self::REMOTE_DATE_FORMAT),
'timeZone' => 'UTC',
],
'allowNewTimeProposals' => false,
'transactionId' => 'calendar_' . $calendar->getId(),
'body' => [
'contentType' => 'text',
'content' => $this->engine->render(
'@ChillCalendar/MSGraph/calendar_event_body.html.twig',
['calendar' => $calendar]
),
],
],
$this->calendarToEventAttendeesOnly($calendar)
);
}
public function calendarToEventAttendeesOnly(Calendar $calendar): array
{
return [
'attendees' => $calendar->getInvites()->map(
function (Invite $i) {
return $this->buildInviteToAttendee($i);
}
)->toArray(),
];
}
public function convertAvailabilityToRemoteEvent(array $event): RemoteEvent
{
$startDate =
DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['start']['dateTime'])
->setTimezone($this->defaultDateTimeZone);
$endDate =
DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['end']['dateTime'], $this->remoteDateTimeZone)
->setTimezone($this->defaultDateTimeZone);
return new RemoteEvent(
uniqid('generated_'),
$this->translator->trans('remote_ms_graph.freebusy_statuses.' . $event['status']),
'',
$startDate,
$endDate
);
}
public function convertToRemote(array $event): RemoteEvent
{
$startDate =
DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['start']['dateTime'])
->setTimezone($this->defaultDateTimeZone);
$endDate =
DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['end']['dateTime'], $this->remoteDateTimeZone)
->setTimezone($this->defaultDateTimeZone);
return new RemoteEvent(
$event['id'],
$event['subject'],
'',
$startDate,
$endDate
);
}
public function getLastModifiedDate(array $event): DateTimeImmutable
{
return DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT, $event['lastModifiedDateTime']);
}
/**
* Return a string which format a DateTime to string. To be used in POST requests,.
*/
public static function getRemoteDateTimeSimpleFormat(): string
{
return 'Y-m-d\TH:i:s';
}
public static function getRemoteTimeZone(): DateTimeZone
{
return new DateTimeZone('UTC');
}
private function buildInviteToAttendee(Invite $invite): array
{
return [
'emailAddress' => [
'address' => $invite->getUser()->getEmail(),
'name' => $invite->getUser()->getLabel(),
],
'type' => 'Required',
];
}
}

View File

@ -0,0 +1,557 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\OnBehalfOfUserHttpClient;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\OnBehalfOfUserTokenStorage;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter;
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
use Chill\MainBundle\Entity\User;
use DateTimeImmutable;
use Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
{
private CalendarRangeRepository $calendarRangeRepository;
private LoggerInterface $logger;
private MachineHttpClient $machineHttpClient;
private MapCalendarToUser $mapCalendarToUser;
private RemoteEventConverter $remoteEventConverter;
private OnBehalfOfUserTokenStorage $tokenStorage;
private TranslatorInterface $translator;
private UrlGeneratorInterface $urlGenerator;
private OnBehalfOfUserHttpClient $userHttpClient;
public function __construct(
CalendarRangeRepository $calendarRangeRepository,
MachineHttpClient $machineHttpClient,
MapCalendarToUser $mapCalendarToUser,
LoggerInterface $logger,
OnBehalfOfUserTokenStorage $tokenStorage,
OnBehalfOfUserHttpClient $userHttpClient,
RemoteEventConverter $remoteEventConverter,
TranslatorInterface $translator,
UrlGeneratorInterface $urlGenerator
) {
$this->calendarRangeRepository = $calendarRangeRepository;
$this->machineHttpClient = $machineHttpClient;
$this->mapCalendarToUser = $mapCalendarToUser;
$this->logger = $logger;
$this->remoteEventConverter = $remoteEventConverter;
$this->tokenStorage = $tokenStorage;
$this->translator = $translator;
$this->urlGenerator = $urlGenerator;
$this->userHttpClient = $userHttpClient;
}
public function getMakeReadyResponse(string $returnPath): Response
{
return new RedirectResponse($this->urlGenerator
->generate('chill_calendar_remote_connect_azure', ['returnPath' => $returnPath]));
}
public function isReady(): bool
{
return $this->tokenStorage->hasToken();
}
public function listEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): array
{
$userId = $this->mapCalendarToUser->getUserId($user);
if (null === $userId) {
return [];
}
try {
$bareEvents = $this->userHttpClient->request(
'GET',
'users/' . $userId . '/calendarView',
[
'query' => [
'startDateTime' => $startDate->format(DateTimeImmutable::ATOM),
'endDateTime' => $endDate->format(DateTimeImmutable::ATOM),
'$select' => 'id,subject,start,end',
],
]
)->toArray();
$ids = array_map(static function ($item) { return $item['id']; }, $bareEvents['value']);
$existingIdsInRange = $this->calendarRangeRepository->findRemoteIdsPresent($ids);
return array_values(
array_map(
function ($item) {
return $this->remoteEventConverter->convertToRemote($item);
},
// filter all event to keep only the one not in range
array_filter(
$bareEvents['value'],
static function ($item) use ($existingIdsInRange) {
return (!$existingIdsInRange[$item['id']]) ?? true;
}
)
)
);
} catch (ClientExceptionInterface $e) {
if (403 === $e->getResponse()->getStatusCode()) {
return $this->getScheduleTimesForUser($user, $startDate, $endDate);
}
$this->logger->debug('Could not get list of event on MSGraph', [
'error_code' => $e->getResponse()->getStatusCode(),
'error' => $e->getResponse()->getInfo(),
]);
return [];
}
}
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void
{
if ('' === $remoteId) {
return;
}
$this->removeEvent($remoteId, $user);
}
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void
{
/*
* cases to support:
*
* * a calendar range is created:
* * create on remote
* * if calendar range is associated: remove the range
* * a Calendar change the CalendarRange:
* * re-create the previous calendar range;
* * remove the current calendar range
* * a calendar change the mainUser
* * cancel the calendar in the previous mainUser
* * recreate the previous calendar range in the previousMainUser, if any
* * delete the current calendar range in the current mainUser, if any
* * create the calendar in the current mainUser
*
*/
if (!$calendar->hasRemoteId()) {
$this->createCalendarOnRemote($calendar);
} else {
if (null !== $previousMainUser) {
// cancel event in previousMainUserCalendar
$this->cancelOnRemote(
$calendar->getRemoteId(),
$this->translator->trans('remote_ms_graph.cancel_event_because_main_user_is_%label%', ['%label%' => $calendar->getMainUser()]),
$previousMainUser,
'calendar_' . $calendar->getRemoteId()
);
$this->createCalendarOnRemote($calendar);
} else {
$this->patchCalendarOnRemote($calendar, $newInvites);
}
}
if ($calendar->hasCalendarRange() && $calendar->getCalendarRange()->hasRemoteId()) {
$this->removeEvent(
$calendar->getCalendarRange()->getRemoteId(),
$calendar->getCalendarRange()->getUser()
);
$calendar->getCalendarRange()
->addRemoteAttributes([
'lastModifiedDateTime' => null,
'changeKey' => null,
'previousId' => $calendar->getCalendarRange()->getRemoteId(),
])
->setRemoteId('');
}
if (null !== $previousCalendarRange) {
$this->createRemoteCalendarRange($previousCalendarRange);
}
}
public function syncCalendarRange(CalendarRange $calendarRange): void
{
if ($calendarRange->hasRemoteId()) {
$this->updateRemoteCalendarRange($calendarRange);
} else {
$this->createRemoteCalendarRange($calendarRange);
}
}
private function cancelOnRemote(string $remoteId, string $comment, User $user, string $identifier): void
{
$userId = $this->mapCalendarToUser->getUserId($user);
if (null === $userId) {
return;
}
try {
$this->machineHttpClient->request(
'POST',
"users/{$userId}/calendar/events/{$remoteId}/cancel",
[
'json' => ['Comment' => $comment],
]
);
} catch (ClientExceptionInterface $e) {
$this->logger->warning('could not update calendar range to remote', [
'exception' => $e->getTraceAsString(),
'content' => $e->getResponse()->getContent(),
'calendarRangeId' => $identifier,
]);
throw $e;
}
}
private function createCalendarOnRemote(Calendar $calendar): void
{
$eventData = $this->remoteEventConverter->calendarToEvent($calendar);
[
'id' => $id,
'lastModifiedDateTime' => $lastModified,
'changeKey' => $changeKey
] = $this->createOnRemote($eventData, $calendar->getMainUser(), 'calendar_' . $calendar->getId());
if (null === $id) {
return;
}
$calendar
->setRemoteId($id)
->addRemoteAttributes([
'lastModifiedDateTime' => $lastModified,
'changeKey' => $changeKey,
]);
}
/**
* @param string $identifier an identifier for logging in case of something does not work
*
* @return array{?id: string, ?lastModifiedDateTime: int, ?changeKey: string}
*/
private function createOnRemote(array $eventData, User $user, string $identifier): array
{
$userId = $this->mapCalendarToUser->getUserId($user);
if (null === $userId) {
$this->logger->warning('user does not have userId nor calendarId', [
'user_id' => $user->getId(),
'calendar_identifier' => $identifier,
]);
return [];
}
try {
$event = $this->machineHttpClient->request(
'POST',
'users/' . $userId . '/calendar/events',
[
'json' => $eventData,
]
)->toArray();
} catch (ClientExceptionInterface $e) {
$this->logger->warning('could not save calendar range to remote', [
'exception' => $e->getTraceAsString(),
'content' => $e->getResponse()->getContent(),
'calendar_identifier' => $identifier,
]);
throw $e;
}
return [
'id' => $event['id'],
'lastModifiedDateTime' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(),
'changeKey' => $event['changeKey'],
];
}
private function createRemoteCalendarRange(CalendarRange $calendarRange): void
{
$userId = $this->mapCalendarToUser->getUserId($calendarRange->getUser());
if (null === $userId) {
$this->logger->warning('user does not have userId nor calendarId', [
'user_id' => $calendarRange->getUser()->getId(),
'calendar_range_id' => $calendarRange->getId(),
]);
return;
}
$eventData = $this->remoteEventConverter->calendarRangeToEvent($calendarRange);
[
'id' => $id,
'lastModifiedDateTime' => $lastModified,
'changeKey' => $changeKey
] = $this->createOnRemote(
$eventData,
$calendarRange->getUser(),
'calendar_range_' . $calendarRange->getId()
);
$calendarRange->setRemoteId($id)
->addRemoteAttributes([
'lastModifiedDateTime' => $lastModified,
'changeKey' => $changeKey,
]);
}
private function getOnRemote(User $user, string $remoteId): array
{
$userId = $this->mapCalendarToUser->getUserId($user);
if (null === $userId) {
throw new Exception('no remote calendar for this user', [
'user' => $user->getId(),
'remoteId' => $remoteId,
]);
}
try {
return $this->machineHttpClient->request(
'GET',
'users/' . $userId . '/calendar/events/' . $remoteId
)->toArray();
} catch (ClientExceptionInterface $e) {
$this->logger->warning('Could not get event from calendar', [
'remoteId' => $remoteId,
]);
throw $e;
}
}
private function getScheduleTimesForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): array
{
$userId = $this->mapCalendarToUser->getUserId($user);
if (null === $userId) {
return [];
}
if (null === $user->getEmailCanonical() || '' === $user->getEmailCanonical()) {
return [];
}
$body = [
'schedules' => [$user->getEmailCanonical()],
'startTime' => [
'dateTime' => ($startDate->setTimezone(RemoteEventConverter::getRemoteTimeZone())->format(RemoteEventConverter::getRemoteDateTimeSimpleFormat())),
'timeZone' => 'UTC',
],
'endTime' => [
'dateTime' => ($endDate->setTimezone(RemoteEventConverter::getRemoteTimeZone())->format(RemoteEventConverter::getRemoteDateTimeSimpleFormat())),
'timeZone' => 'UTC',
],
];
try {
$response = $this->userHttpClient->request('POST', 'users/' . $userId . '/calendar/getSchedule', [
'json' => $body,
])->toArray();
} catch (ClientExceptionInterface $e) {
$this->logger->debug('Could not get schedule on MSGraph', [
'error_code' => $e->getResponse()->getStatusCode(),
'error' => $e->getResponse()->getInfo(),
]);
return [];
}
return array_map(
function ($item) {
return $this->remoteEventConverter->convertAvailabilityToRemoteEvent($item);
},
$response['value'][0]['scheduleItems']
);
}
private function patchCalendarOnRemote(Calendar $calendar, array $newInvites): void
{
$eventDatas = [];
$eventDatas[] = $this->remoteEventConverter->calendarToEvent($calendar);
if (0 < count($newInvites)) {
$eventDatas[] = $this->remoteEventConverter->calendarToEventAttendeesOnly($calendar);
}
foreach ($eventDatas as $eventData) {
[
'id' => $id,
'lastModifiedDateTime' => $lastModified,
'changeKey' => $changeKey
] = $this->patchOnRemote(
$calendar->getRemoteId(),
$eventData,
$calendar->getMainUser(),
'calendar_' . $calendar->getId()
);
$calendar->addRemoteAttributes([
'lastModifiedDateTime' => $lastModified,
'changeKey' => $changeKey,
]);
}
}
/**
* @param string $identifier an identifier for logging in case of something does not work
*
* @return array{?id: string, ?lastModifiedDateTime: int, ?changeKey: string}
*/
private function patchOnRemote(string $remoteId, array $eventData, User $user, string $identifier): array
{
$userId = $this->mapCalendarToUser->getUserId($user);
if (null === $userId) {
$this->logger->warning('user does not have userId nor calendarId', [
'user_id' => $user->getId(),
'calendar_identifier' => $identifier,
]);
return [];
}
try {
$event = $this->machineHttpClient->request(
'PATCH',
'users/' . $userId . '/calendar/events/' . $remoteId,
[
'json' => $eventData,
]
)->toArray();
} catch (ClientExceptionInterface $e) {
$this->logger->warning('could not update calendar range to remote', [
'exception' => $e->getTraceAsString(),
'calendarRangeId' => $identifier,
]);
throw $e;
}
return [
'id' => $event['id'],
'lastModifiedDateTime' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(),
'changeKey' => $event['changeKey'],
];
}
private function removeEvent($remoteId, User $user): void
{
$userId = $this->mapCalendarToUser->getUserId($user);
try {
$this->machineHttpClient->request(
'DELETE',
'users/' . $userId . '/calendar/events/' . $remoteId
);
} catch (ClientExceptionInterface $e) {
$this->logger->warning('could not remove event from calendar', [
'event_remote_id' => $remoteId,
'user_id' => $user->getId(),
]);
}
}
private function updateRemoteCalendarRange(CalendarRange $calendarRange): void
{
$userId = $this->mapCalendarToUser->getUserId($calendarRange->getUser());
$calendarId = $this->mapCalendarToUser->getCalendarId($calendarRange->getUser());
if (null === $userId || null === $calendarId) {
$this->logger->warning('user does not have userId nor calendarId', [
'user_id' => $calendarRange->getUser()->getId(),
'calendar_range_id' => $calendarRange->getId(),
]);
return;
}
try {
$event = $this->machineHttpClient->request(
'GET',
'users/' . $userId . '/calendar/events/' . $calendarRange->getRemoteId()
)->toArray();
} catch (ClientExceptionInterface $e) {
$this->logger->warning('Could not get event from calendar', [
'calendar_range_id' => $calendarRange->getId(),
'calendar_range_remote_id' => $calendarRange->getRemoteId(),
]);
throw $e;
}
if ($this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp() > $calendarRange->getUpdatedAt()->getTimestamp()) {
$this->logger->info('Skip updating as the lastModified date seems more fresh than the database one', [
'calendar_range_id' => $calendarRange->getId(),
'calendar_range_remote_id' => $calendarRange->getRemoteId(),
'db_last_updated' => $calendarRange->getUpdatedAt()->getTimestamp(),
'remote_last_updated' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(),
]);
return;
}
$eventData = $this->remoteEventConverter->calendarRangeToEvent($calendarRange);
try {
$event = $this->machineHttpClient->request(
'PATCH',
'users/' . $userId . '/calendar/events/' . $calendarRange->getRemoteId(),
[
'json' => $eventData,
]
)->toArray();
} catch (ClientExceptionInterface $e) {
$this->logger->warning('could not update calendar range to remote', [
'exception' => $e->getTraceAsString(),
'calendarRangeId' => $calendarRange->getId(),
]);
throw $e;
}
$calendarRange
->addRemoteAttributes([
'lastModifiedDateTime' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(),
'changeKey' => $event['changeKey'],
]);
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\MainBundle\Entity\User;
use DateTimeImmutable;
use LogicException;
use Symfony\Component\HttpFoundation\Response;
class NullRemoteCalendarConnector implements RemoteCalendarConnectorInterface
{
public function getMakeReadyResponse(string $returnPath): Response
{
throw new LogicException('As this connector is always ready, this method should not be called');
}
public function isReady(): bool
{
return true;
}
public function listEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): array
{
return [];
}
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void
{
}
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void
{
}
public function syncCalendarRange(CalendarRange $calendarRange): void
{
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
use Chill\MainBundle\Entity\User;
use DateTimeImmutable;
use Symfony\Component\HttpFoundation\Response;
interface RemoteCalendarConnectorInterface
{
/**
* Return a response, more probably a RedirectResponse, where the user
* will be able to fullfill requirements to prepare this connector and
* make it ready.
*/
public function getMakeReadyResponse(string $returnPath): Response;
/**
* Return true if the connector is ready to act as a proxy for reading
* remote calendars.
*/
public function isReady(): bool;
/**
* @return array|RemoteEvent[]
*/
public function listEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): array;
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void;
/**
* @param array<array{inviteId: int, userId: int, userEmail: int, userLabel: string}> $oldInvites
*/
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void;
public function syncCalendarRange(CalendarRange $calendarRange): void;
}

View File

@ -0,0 +1,65 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\DependencyInjection;
use Chill\CalendarBundle\Command\AzureGrantAdminConsentAndAcquireToken;
use Chill\CalendarBundle\Command\MapUserCalendarCommand;
use Chill\CalendarBundle\Controller\RemoteCalendarConnectAzureController;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraphRemoteCalendarConnector;
use Chill\CalendarBundle\RemoteCalendar\Connector\NullRemoteCalendarConnector;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use RuntimeException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use TheNetworg\OAuth2\Client\Provider\Azure;
class RemoteCalendarCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$config = $container->getParameter('chill_calendar');
$connector = null;
if (!$config['remote_calendars_sync']['enabled']) {
$connector = NullRemoteCalendarConnector::class;
}
if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) {
$connector = MSGraphRemoteCalendarConnector::class;
} else {
// remove services which cannot be loaded
$container->removeDefinition(MapUserCalendarCommand::class);
$container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class);
$container->removeDefinition(RemoteCalendarConnectAzureController::class);
}
if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) {
$container->setAlias(Azure::class, 'knpu.oauth2.provider.azure');
}
if (null === $connector) {
throw new RuntimeException('Could not configure remote calendar');
}
foreach ([
NullRemoteCalendarConnector::class,
MSGraphRemoteCalendarConnector::class, ] as $serviceId) {
if ($connector === $serviceId) {
$container->getDefinition($serviceId)
->setDecoratedService(RemoteCalendarConnectorInterface::class);
} else {
// keep the container lighter by removing definitions
$container->removeDefinition($serviceId);
}
}
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Model;
use DateTimeImmutable;
use Symfony\Component\Serializer\Annotation as Serializer;
class RemoteEvent
{
public string $description;
/**
* @Serializer\Groups({"read"})
*/
public DateTimeImmutable $endDate;
/**
* @Serializer\Groups({"read"})
*/
public string $id;
/**
* @Serializer\Groups({"read"})
*/
public DateTimeImmutable $startDate;
/**
* @Serializer\Groups({"read"})
*/
public string $title;
public function __construct(string $id, string $title, string $description, DateTimeImmutable $startDate, DateTimeImmutable $endDate)
{
$this->id = $id;
$this->title = $title;
$this->description = $description;
$this->startDate = $startDate;
$this->endDate = $endDate;
}
}

View File

@ -12,48 +12,159 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Repository; namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\CalendarRange; use Chill\CalendarBundle\Entity\CalendarRange;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Chill\MainBundle\Entity\User;
use Doctrine\Persistence\ManagerRegistry; use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
use function count;
class CalendarRangeRepository implements ObjectRepository
{
private EntityManagerInterface $em;
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
$this->repository = $entityManager->getRepository(CalendarRange::class);
}
public function countByAvailableRangesForUser(User $user, DateTimeImmutable $from, DateTimeImmutable $to): int
{
return $this->buildQueryAvailableRangesForUser($user, $from, $to)
->select('COUNT(cr)')
->getQuery()->getSingleScalarResult();
}
public function find($id): ?CalendarRange
{
return $this->repository->find($id);
}
/** /**
* @method CalendarRange|null find($id, $lockMode = null, $lockVersion = null) * @return array|CalendarRange[]
* @method CalendarRange|null findOneBy(array $criteria, array $orderBy = null)
* @method CalendarRange[] findAll()
* @method CalendarRange[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */
class CalendarRangeRepository extends ServiceEntityRepository public function findAll(): array
{ {
public function __construct(ManagerRegistry $registry) return $this->repository->findAll();
{
parent::__construct($registry, CalendarRange::class);
} }
// /** /**
// * @return CalendarRange[] Returns an array of CalendarRange objects * @return array|CalendarRange[]
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('c')
->andWhere('c.exampleField = :val')
->setParameter('val', $value)
->orderBy('c.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/ */
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
/* /**
public function findOneBySomeField($value): ?CalendarRange * @return array|CalendarRange[]
{
return $this->createQueryBuilder('c')
->andWhere('c.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/ */
public function findByAvailableRangesForUser(
User $user,
DateTimeImmutable $from,
DateTimeImmutable $to,
?int $limit = null,
?int $offset = null
): array {
$qb = $this->buildQueryAvailableRangesForUser($user, $from, $to);
if (null !== $limit) {
$qb->setMaxResults($limit);
}
if (null !== $offset) {
$qb->setFirstResult($offset);
}
return $qb->getQuery()->getResult();
}
public function findOneBy(array $criteria): ?CalendarRange
{
return $this->repository->findOneBy($criteria);
}
/**
* Given a list of remote ids, return an array where
* keys are the remoteIds, and value is a boolean, true if the
* id is present in database.
*
* @param array<int, string>|list<string> $remoteIds
*
* @return array<string, bool>
*/
public function findRemoteIdsPresent(array $remoteIds): array
{
if (0 === count($remoteIds)) {
return [];
}
$sql = 'SELECT
sq.remoteId as remoteid,
EXISTS (SELECT 1 FROM chill_calendar.calendar_range cr WHERE cr.remoteId = sq.remoteId) AS present
FROM
(
VALUES %remoteIds%
) AS sq(remoteId);
';
$remoteIdsStr = implode(
', ',
array_fill(0, count($remoteIds), '((?))')
);
$rsm = new ResultSetMapping();
$rsm
->addScalarResult('remoteid', 'remoteId', Types::STRING)
->addScalarResult('present', 'present', Types::BOOLEAN);
$rows = $this->em
->createNativeQuery(
strtr($sql, ['%remoteIds%' => $remoteIdsStr]),
$rsm
)
->setParameters(array_values($remoteIds))
->getResult();
$results = [];
foreach ($rows as $r) {
$results[$r['remoteId']] = $r['present'];
}
return $results;
}
public function getClassName(): string
{
return CalendarRange::class;
}
private function buildQueryAvailableRangesForUser(User $user, DateTimeImmutable $from, DateTimeImmutable $to): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('cr');
$qb->leftJoin('cr.calendar', 'calendar');
return $qb
->where(
$qb->expr()->andX(
$qb->expr()->eq('cr.user', ':user'),
$qb->expr()->gte('cr.startDate', ':startDate'),
$qb->expr()->lte('cr.endDate', ':endDate'),
$qb->expr()->isNull('calendar')
)
)
->setParameters([
'user' => $user,
'startDate' => $from,
'endDate' => $to,
]);
}
} }

View File

@ -12,52 +12,68 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Repository; namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Entity\Calendar;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ObjectRepository;
class CalendarRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(Calendar::class);
}
public function countByAccompanyingPeriod(AccompanyingPeriod $period): int
{
return $this->repository->count(['accompanyingPeriod' => $period]);
}
public function find($id): ?Calendar
{
return $this->repository->find($id);
}
/** /**
* @method Calendar|null find($id, $lockMode = null, $lockVersion = null) * @return array|Calendar[]
* @method Calendar|null findOneBy(array $criteria, array $orderBy = null)
* @method Calendar[] findAll()
* @method Calendar[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */
class CalendarRepository extends ServiceEntityRepository public function findAll(): array
{ {
// private EntityRepository $repository; return $this->repository->findAll();
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Calendar::class);
// $this->repository = $entityManager->getRepository(AccompanyingPeriodWork::class);
} }
// /** /**
// * @return Calendar[] Returns an array of Calendar objects * @return array|Calendar[]
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('c')
->andWhere('c.exampleField = :val')
->setParameter('val', $value)
->orderBy('c.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/ */
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 findOneBySomeField($value): ?Calendar * @return array|Calendar[]
{
return $this->createQueryBuilder('c')
->andWhere('c.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/ */
public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->findBy(
[
'accompanyingPeriod' => $period,
],
$orderBy,
$limit,
$orderBy
);
}
public function findOneBy(array $criteria): ?Calendar
{
return $this->repository->findOneBy($criteria);
}
public function getClassName()
{
return Calendar::class;
}
} }

View File

@ -12,48 +12,47 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Repository; namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\Invite; use Chill\CalendarBundle\Entity\Invite;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
class InviteRepository implements ObjectRepository
{
private EntityRepository $entityRepository;
public function __construct(EntityManagerInterface $em)
{
$this->entityRepository = $em->getRepository(Invite::class);
}
public function find($id): ?Invite
{
return $this->entityRepository->find($id);
}
/** /**
* @method Invite|null find($id, $lockMode = null, $lockVersion = null) * @return array|Invite[]
* @method Invite|null findOneBy(array $criteria, array $orderBy = null)
* @method Invite[] findAll()
* @method Invite[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */
class InviteRepository extends ServiceEntityRepository public function findAll(): array
{ {
public function __construct(ManagerRegistry $registry) return $this->entityRepository->findAll();
{
parent::__construct($registry, Invite::class);
} }
// /** /**
// * @return Invite[] Returns an array of Invite objects * @return array|Invite[]
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('i')
->andWhere('i.exampleField = :val')
->setParameter('val', $value)
->orderBy('i.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/ */
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
{
return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset);
}
/* public function findOneBy(array $criteria): ?Invite
public function findOneBySomeField($value): ?Invite
{ {
return $this->createQueryBuilder('i') return $this->entityRepository->findOneBy($criteria);
->andWhere('i.exampleField = :val') }
->setParameter('val', $value)
->getQuery() public function getClassName(): string
->getOneOrNullResult() {
; return Invite::class;
} }
*/
} }

View File

@ -13,6 +13,23 @@ services:
resource: '../../Menu/' resource: '../../Menu/'
tags: ['chill.menu_builder'] tags: ['chill.menu_builder']
Chill\CalendarBundle\Command\:
autowire: true
autoconfigure: true
resource: '../../Command/'
Chill\CalendarBundle\Messenger\:
autowire: true
autoconfigure: true
resource: '../../Messenger/'
Chill\CalendarBundle\Command\AzureGrantAdminConsentAndAcquireToken:
autoconfigure: true
autowire: true
arguments:
$azure: '@knpu.oauth2.provider.azure'
tags: ['console.command']
Chill\CalendarBundle\Security\: Chill\CalendarBundle\Security\:
autoconfigure: true autoconfigure: true
autowire: true autowire: true

View File

@ -8,3 +8,36 @@ services:
event: 'postPersist' event: 'postPersist'
entity: 'Chill\ActivityBundle\Entity\Activity' entity: 'Chill\ActivityBundle\Entity\Activity'
Chill\CalendarBundle\Messenger\Doctrine\CalendarRangeEntityListener:
autowire: true
autoconfigure: true
tags:
-
name: 'doctrine.orm.entity_listener'
event: 'postPersist'
entity: 'Chill\CalendarBundle\Entity\CalendarRange'
-
name: 'doctrine.orm.entity_listener'
event: 'postUpdate'
entity: 'Chill\CalendarBundle\Entity\CalendarRange'
-
name: 'doctrine.orm.entity_listener'
event: 'postRemove'
entity: 'Chill\CalendarBundle\Entity\CalendarRange'
Chill\CalendarBundle\Messenger\Doctrine\CalendarEntityListener:
autowire: true
autoconfigure: true
tags:
-
name: 'doctrine.orm.entity_listener'
event: 'postPersist'
entity: 'Chill\CalendarBundle\Entity\Calendar'
-
name: 'doctrine.orm.entity_listener'
event: 'postUpdate'
entity: 'Chill\CalendarBundle\Entity\Calendar'
-
name: 'doctrine.orm.entity_listener'
event: 'postRemove'
entity: 'Chill\CalendarBundle\Entity\Calendar'

View File

@ -1,10 +1,6 @@
--- ---
services: services:
chill.calendar.form.type.calendar: Chill\CalendarBundle\Form\:
class: Chill\CalendarBundle\Form\CalendarType resource: './../../Form'
arguments: autowire: true
- "@chill.main.helper.translatable_string" autoconfigure: true
- "@doctrine.orm.entity_manager"
tags:
- { name: form.type, alias: chill_calendarbundle_calendar }

View File

@ -0,0 +1,15 @@
services:
_defaults:
autoconfigure: true
autowire: true
Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface: ~
Chill\CalendarBundle\RemoteCalendar\Connector\NullRemoteCalendarConnector: ~
Chill\CalendarBundle\RemoteCalendar\Connector\MSGraphRemoteCalendarConnector: ~
Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\:
resource: '../../RemoteCalendar/Connector/MSGraph/'

View File

@ -0,0 +1,22 @@
import { createApp } from 'vue';
import Answer from 'ChillCalendarAssets/vuejs/Invite/Answer';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
const i18n = _createI18n({});
document.addEventListener('DOMContentLoaded', function (e) {
console.log('dom loaded answer');
document.querySelectorAll('div[invite-answer]').forEach(function (el) {
console.log('element found', el);
const app = createApp({
components: {
Answer,
},
template: '<answer :calendarId="14" :status="defined"></answer>',
});
app.use(i18n).mount(el);
});
});

View File

@ -1,16 +1,36 @@
<template> <template>
<div class="row">
<div class="col-md-4">
<label>Utilisateur principal</label>
</div>
<div class="col-md-8 align-content-end">
<pick-entity
:multiple="false"
:types="['user']"
:uniqid="'main_user_calendar'"
:picked="null !== this.$store.getters.getMainUser ? [this.$store.getters.getMainUser] : []"
:removableIfSet="false"
@addNewEntity="setMainUser"
></pick-entity>
</div>
</div>
<div class="row">
<select v-model="this.slotDuration">
<option value="00:05:00">5</option>
<option value="00:10:00">10</option>
<option value="00:15:00">15</option>
<option value="00:30:00">30</option>
</select>
</div>
<div>
<input type="checkbox" v-model="this.hideWeekEnds" /><label>Masquer les week-ends</label>
</div>
<concerned-groups></concerned-groups> <concerned-groups></concerned-groups>
<location></location> <div>
<teleport to="#calendarControls"> <template v-for="u in getActiveUsers" :key="u.id">
<calendar-user-selector <calendar-active :user="u" ></calendar-active>
v-bind:users="users" </template>
v-bind:calendarEvents="calendarEvents" </div>
v-bind:updateEventsSource="updateEventsSource"
v-bind:showMyCalendar="showMyCalendar"
v-bind:toggleMyCalendar="toggleMyCalendar"
v-bind:toggleWeekends="toggleWeekends" >
</calendar-user-selector>
</teleport>
<teleport to="#fullCalendar"> <teleport to="#fullCalendar">
<FullCalendar ref="fullCalendar" :options="calendarOptions"> <FullCalendar ref="fullCalendar" :options="calendarOptions">
<template v-slot:eventContent='arg'> <template v-slot:eventContent='arg'>
@ -19,9 +39,11 @@
</template> </template>
</FullCalendar> </FullCalendar>
</teleport> </teleport>
<location></location>
</template> </template>
<script> <script>
//import {mapGetters} from 'vuex';
import ConcernedGroups from 'ChillActivityAssets/vuejs/Activity/components/ConcernedGroups.vue'; import ConcernedGroups from 'ChillActivityAssets/vuejs/Activity/components/ConcernedGroups.vue';
import Location from 'ChillActivityAssets/vuejs/Activity/components/Location.vue'; import Location from 'ChillActivityAssets/vuejs/Activity/components/Location.vue';
import CalendarUserSelector from '../_components/CalendarUserSelector/CalendarUserSelector.vue'; import CalendarUserSelector from '../_components/CalendarUserSelector/CalendarUserSelector.vue';
@ -31,126 +53,141 @@ import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid'; import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction'; import interactionPlugin from '@fullcalendar/interaction';
import timeGridPlugin from '@fullcalendar/timegrid'; import timeGridPlugin from '@fullcalendar/timegrid';
// import listPlugin from '@fullcalendar/list'; import CalendarActive from './Components/CalendarActive';
import PickEntity from 'ChillMainAssets/vuejs/PickEntity/PickEntity.vue';
import {mapGetters} from "vuex";
export default { export default {
name: "App", name: "App",
components: { components: {
ConcernedGroups, ConcernedGroups,
Location, Location,
CalendarUserSelector, FullCalendar,
FullCalendar CalendarActive,
PickEntity,
}, },
data() { data() {
return { return {
errorMsg: [], errorMsg: [],
users: { showMyCalendar: false,
loaded: [], slotDuration: '00:10:00',
selected: [], hideWeekEnds: true,
logged: null
},
calendarEvents: {
loaded: [],
selected: [],
user: [],
current: {
events: [{
title: 'plage prévue',
start: window.startDate,
end: window.endDate
}],
id: window.mainUser,
color: '#bbbbbb'
} }
}, },
selectedEvent: null, computed: {
previousSelectedEvent: null, ...mapGetters(['getMainUser']),
previousSelectedEventColor: null, events() {
showMyCalendar: false, return this.$store.getters.getEventSources;
calendarOptions: { },
calendarOptions() {
return {
locale: frLocale, locale: frLocale,
plugins: [ dayGridPlugin, interactionPlugin, timeGridPlugin ], plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, dayGridPlugin],
initialView: 'timeGridWeek', initialView: 'timeGridWeek',
initialDate: window.startDate !== undefined ? window.startDate : new Date(), initialDate: this.$store.getters.initialDate,
eventSource: [], eventSources: this.events,
selectable: true, selectable: true,
datesSet: this.onDatesSet,
select: this.onDateSelect, select: this.onDateSelect,
eventChange: this.onEventChange, eventChange: this.onEventChange,
eventClick: this.onEventClick, eventClick: this.onEventClick,
// eventMouseEnter: this.onEventMouseEnter,
// eventMouseLeave: this.onEventMouseLeave,
selectMirror: true, selectMirror: true,
editable: true, editable: true,
weekends: false, weekends: !this.hideWeekEnds,
headerToolbar: { headerToolbar: {
left: 'prev,next today', left: 'prev,next today',
center: 'title', center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth,listWeek,listDay' right: 'dayGridMonth,timeGridWeek,dayGridThreeDays,timeGridDay',
}, },
views: {
timeGrid: {
slotEventOverlap: false,
slotDuration: this.slotDuration,
scrollTime: '10:00:00',
},
dayGridThreeDays: {
type: 'dayGridWeek',
duration: { days: 3},
buttonText: this.$t('list_three_days'),
},
},
};
},
getActiveUsers() {
const users = [];
for (const id of this.$store.state.currentView.users.keys()) {
users.push(this.$store.getters.getUserDataById(id).user);
} }
return users;
} }
}, },
methods: { methods: {
init() { setMainUser(user) {
this.updateEventsSource(); console.log('setMainUser APP', user);
},
toggleMyCalendar(value) { if (user.id !== this.$store.getters.getMainUser && (
this.showMyCalendar = value; this.$store.state.activity.calendarRange !== null
}, || this.$store.state.activity.startDate !== null
toggleWeekends: function() { || this.$store.state.activity.endDate !== null
this.calendarOptions.weekends = !this.calendarOptions.weekends; )
}, ) {
updateEventsSource() { if (!window.confirm(this.$t('change_main_user_will_reset_event_data'))) {
this.calendarOptions.eventSources = []; return;
this.calendarOptions.eventSources.push(...this.calendarEvents.selected);
if (window.startDate !== undefined) {
this.calendarOptions.eventSources.push(this.calendarEvents.current);
}
if (this.showMyCalendar) {
this.calendarOptions.eventSources.push(this.calendarEvents.user);
}
},
unSelectPreviousEvent(event) {
if (event) {
if (typeof event.setProp === 'function') {
event.setProp('backgroundColor', this.previousSelectedEventColor);
event.setProp('borderColor', this.previousSelectedEventColor);
event.setProp('textColor','#444444');
event.setProp('title','');
} }
} }
this.$store.dispatch('setMainUser', user);
this.$store.commit('showUserOnCalendar', {user, ranges: true, remotes: true});
},
removeMainUser(user) {
console.log('removeMainUser APP', user);
window.alert(this.$t('main_user_is_mandatory'));
return;
},
onDatesSet(event) {
console.log('onDatesSet', event);
this.$store.dispatch('setCurrentDatesView', {start: event.start, end: event.end});
}, },
onDateSelect(payload) { onDateSelect(payload) {
console.log(payload) console.log('onDateSelect', payload);
this.unSelectPreviousEvent(this.selectedEvent);
Object.assign(payload, {users: this.users}); // show an alert if changing mainUser
Object.assign(payload, {title: 'Choisir cette plage'}); //TODO does not display if (this.$store.getters.getMainUser !== null
//payload.event.setProp('title', 'Choisir cette plage'); && this.$store.state.me.id !== this.$store.getters.getMainUser.id) {
this.$store.dispatch('createEvent', payload); if (!window.confirm(this.$t('will_change_main_user_for_me'))) {
return;
} else {
this.$store.commit('showUserOnCalendar', {user: this.$store.state.me, remotes: true, ranges: true})
}
}
this.$store.dispatch('setEventTimes', {start: payload.start, end: payload.end});
}, },
onEventChange(payload) { onEventChange(payload) {
this.$store.dispatch('updateEvent', payload); console.log('onEventChange', payload);
if (this.$store.state.activity.calendarRange !== null) {
throw new Error("not allowed to edit a calendar associated with a calendar range");
}
this.$store.dispatch('setEventTimes', {start: payload.event.start, end: payload.event.end});
}, },
onEventClick(payload) { onEventClick(payload) {
this.previousSelectedEvent = this.selectedEvent; if (payload.event.extendedProps.is !== 'range') {
this.previousSelectedEventColor = payload.event.extendedProps.sourceColor; // do nothing when clicking on remote
this.selectedEvent = payload.event; return;
this.unSelectPreviousEvent(this.previousSelectedEvent);
payload.event.setProp('backgroundColor','#3788d8');
payload.event.setProp('borderColor','#3788d8');
payload.event.setProp('title', 'Choisir cette plage');
payload.event.setProp('textColor','#ffffff');
},
onEventMouseEnter(payload) {
payload.event.setProp('borderColor','#444444');
},
onEventMouseLeave(payload) {
payload.event.setProp('borderColor','#ffffff');
} }
},
mounted() { // show an alert if changing mainUser
this.init(); if (this.$store.getters.getMainUser !== null
&& payload.event.extendedProps.userId !== this.$store.getters.getMainUser.id) {
if (!window.confirm(this.$t('this_calendar_range_will_change_main_user'))) {
return;
} }
} }
this.$store.dispatch('associateCalendarToRange', {range: payload.event});
},
},
}
</script> </script>

View File

@ -0,0 +1,50 @@
<template>
<span class="badge" :style="style">
{{ user.text }}
<span class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault" v-model="rangeShow">
<label class="form-check-label" for="flexSwitchCheckDefault">Disponibilité</label>
</span>
<span class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault" v-model="remoteShow">
<label class="form-check-label" for="flexSwitchCheckDefault">Agenda</label>
</span>
</span>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
name: "CalendarActive",
props: ['user'],
computed: {
style() {
return {
backgroundColor: this.$store.getters.getUserData(this.user).mainColor,
color: 'black',
};
},
rangeShow: {
set (value) {
this.$store.commit('showUserOnCalendar', {user: this.user, ranges: value});
},
get() {
return this.$store.getters.isRangeShownOnCalendarForUser(this.user);
}
},
remoteShow: {
set (value) {
this.$store.commit('showUserOnCalendar', {user: this.user, remotes: value});
},
get() {
return this.$store.getters.isRemoteShownOnCalendarForUser(this.user);
}
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,48 @@
import {fetchResults} from 'ChillMainAssets/lib/api/apiMethods';
import {datetimeToISO} from 'ChillMainAssets/chill/js/date';
const whoami = () => {
const url = `/api/1.0/main/whoami.json`;
return fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw {
msg: 'Error while getting whoami.',
sta: response.status,
txt: response.statusText,
err: new Error(),
body: response.body
};
});
};
/**
*
* @param user
* @param Date start
* @param Date end
* @return Promise
*/
const fetchCalendarRangeForUser = (user, start, end) => {
const uri = `/api/1.0/calendar/calendar-range-available/${user.id}.json`;
const dateFrom = datetimeToISO(start);
const dateTo = datetimeToISO(end);
return fetchResults(uri, {dateFrom, dateTo});
}
const fetchCalendarRemoteForUser = (user, start, end) => {
const uri = `/api/1.0/calendar/proxy/calendar/by-user/${user.id}/events`;
const dateFrom = datetimeToISO(start);
const dateTo = datetimeToISO(end);
return fetchResults(uri, {dateFrom, dateTo});
}
export {
whoami,
fetchCalendarRangeForUser,
fetchCalendarRemoteForUser,
};

View File

@ -0,0 +1,19 @@
const COLORS = [ /* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
'#8dd3c7',
'#ffffb3',
'#bebada',
'#fb8072',
'#80b1d3',
'#fdb462',
'#b3de69',
'#fccde5',
'#d9d9d9',
'#bc80bd',
'#ccebc5',
'#ffed6f'
];
export {
COLORS,
};

View File

@ -12,7 +12,12 @@ const appMessages = {
bloc_persons_not_associated: "Tiers non-pro.", bloc_persons_not_associated: "Tiers non-pro.",
bloc_thirdparty: "Tiers professionnels", bloc_thirdparty: "Tiers professionnels",
bloc_users: "T(M)S", bloc_users: "T(M)S",
} },
this_calendar_range_will_change_main_user: "Cette plage de disponibilité n'est pas celle de l'utilisateur principal. Si vous continuez, l'utilisateur principal sera adapté. Êtes-vous sûr·e ?",
will_change_main_user_for_me: "Vous ne pouvez pas écrire dans le calendrier d'un autre utilisateur. Voulez-vous être l'utilisateur principal de ce rendez-vous ?",
main_user_is_mandatory: "L'utilisateur principal est requis. Vous pouvez le modifier, mais pas le supprimer",
change_main_user_will_reset_event_data: "Modifier l'utilisateur principal nécessite de choisir une autre plage de disponibilité ou un autre horaire. Ces informations seront perdues. Êtes-vous sûr·e de vouloir continuer ?",
list_three_days: 'Liste 3 jours',
} }
} }

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