mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-07-01 14:36:13 +00:00
Merge branch 'master' into 232_resources_comment
This commit is contained in:
commit
b23161fa1d
@ -40,6 +40,9 @@ and this project adheres to
|
||||
* address reference: add index for refid
|
||||
* [accompanyingCourse_work] fix styles conflicts + fix bug with remove goal (remove goals one at a time)
|
||||
* [accompanyingCourse] improve masonry on resume page, add origin
|
||||
* [notification] new notification interface, can be associated to AccompanyingCourse/Period, Activities.
|
||||
* List notifications, show, and comment in User section
|
||||
* Notify button and contextual notification box on associated objects pages
|
||||
|
||||
## Test releases
|
||||
|
||||
|
@ -33,7 +33,8 @@
|
||||
"symfony/form": "^4.4",
|
||||
"symfony/framework-bundle": "^4.4",
|
||||
"symfony/intl": "^4.4",
|
||||
"symfony/mime": "^4.4",
|
||||
"symfony/mailer": "^5.4",
|
||||
"symfony/mime": "^5.4",
|
||||
"symfony/monolog-bundle": "^3.5",
|
||||
"symfony/security-bundle": "^4.4",
|
||||
"symfony/serializer": "^5.3",
|
||||
@ -47,7 +48,12 @@
|
||||
"symfony/yaml": "^4.4",
|
||||
"twig/extra-bundle": "^3.0",
|
||||
"twig/intl-extra": "^3.0",
|
||||
"twig/markdown-extra": "^3.3"
|
||||
"twig/markdown-extra": "^3.3",
|
||||
"twig/string-extra": "^3.3",
|
||||
"twig/twig": "^3.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.3",
|
||||
@ -65,8 +71,17 @@
|
||||
"symfony/var-dumper": "^4.4",
|
||||
"symfony/web-profiler-bundle": "^4.4"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
"config": {
|
||||
"bin-dir": "bin",
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true,
|
||||
"vendor-dir": "tests/app/vendor",
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"phpstan/extension-installer": true,
|
||||
"ergebnis/composer-normalize": true,
|
||||
"phpro/grumphp": true
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
@ -0,0 +1,45 @@
|
||||
<?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\ActivityBundle\Notification;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||
|
||||
final class ActivityNotificationHandler implements NotificationHandlerInterface
|
||||
{
|
||||
private ActivityRepository $activityRepository;
|
||||
|
||||
public function __construct(ActivityRepository $activityRepository)
|
||||
{
|
||||
$this->activityRepository = $activityRepository;
|
||||
}
|
||||
|
||||
public function getTemplate(Notification $notification, array $options = []): string
|
||||
{
|
||||
return '@ChillActivity/Activity/showInNotification.html.twig';
|
||||
}
|
||||
|
||||
public function getTemplateData(Notification $notification, array $options = []): array
|
||||
{
|
||||
return [
|
||||
'notification' => $notification,
|
||||
'activity' => $this->activityRepository->find($notification->getRelatedEntityId()),
|
||||
];
|
||||
}
|
||||
|
||||
public function supports(Notification $notification, array $options = []): bool
|
||||
{
|
||||
return $notification->getRelatedEntityClass() === Activity::class;
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<?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\ActivityBundle\Notification;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
|
||||
final class ActivityNotificationRenderer
|
||||
{
|
||||
public function getTemplate()
|
||||
{
|
||||
return '@ChillActivity/Activity/showInNotification.html.twig';
|
||||
}
|
||||
|
||||
public function getTemplateData(Notification $notification)
|
||||
{
|
||||
return ['notification' => $notification];
|
||||
}
|
||||
|
||||
public function supports(Notification $notification, array $options = []): bool
|
||||
{
|
||||
return $notification->getRelatedEntityClass() === Activity::class;
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
{% set t = activity.type %}
|
||||
<div class="item-bloc activity-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
|
||||
|
||||
<div class="item-row">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
{% if activity.date %}
|
||||
<p class="date-label">
|
||||
{{ activity.date|format_date('short') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
<h2 class="badge-title">
|
||||
<span class="title_label"></span>
|
||||
<span class="title_action">
|
||||
{{ activity.type.name | localize_translatable_string }}
|
||||
|
||||
{% if activity.emergency %}
|
||||
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-row column separator">
|
||||
<div class="wrap-list">
|
||||
{% if activity.location and t.locationVisible %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'location'|trans }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
<p class="wl-item">
|
||||
<span>{{ activity.location.locationType.title|localize_translatable_string }}</span>
|
||||
{{ activity.location.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if activity.sentReceived is not empty and t.sentReceivedVisible %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'Sent received'|trans }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
<p class="wl-item">
|
||||
{{ activity.sentReceived|capitalize|trans }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if activity.user and t.userVisible %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
<p class="wl-item">
|
||||
{{ activity.user.usernameCanonical|chill_entity_render_string|capitalize }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
|
||||
'context': context,
|
||||
'with_display': 'wrap-list',
|
||||
'entity': activity,
|
||||
'badge_person': true
|
||||
} %}
|
||||
|
||||
<div class="wrap-list">
|
||||
{%- if activity.reasons is not empty and t.reasonsVisible -%}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<h3>{{ 'Reasons'|trans }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% for r in activity.reasons %}
|
||||
<p class="wl-item reasons">
|
||||
{{ r|chill_entity_render_box }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{%- if activity.socialIssues is not empty and t.socialIssuesVisible -%}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<h3>{{ 'Social issues'|trans }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% for r in activity.socialIssues %}
|
||||
<p class="wl-item social-issues">
|
||||
{{ r|chill_entity_render_box }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{%- if activity.socialActions is not empty and t.socialActionsVisible -%}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<h3>{{ 'Social actions'|trans }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% for r in activity.socialActions %}
|
||||
<p class="wl-item social-actions">
|
||||
{{ r|chill_entity_render_box }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if activity.comment.comment is not empty and is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<h3>{{ 'Comment'|trans }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{{ activity.comment|chill_entity_render_box({
|
||||
'disable_markdown': false,
|
||||
'limit_lines': 3,
|
||||
'metadata': false
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Only if ACL SEE_DETAILS AND/OR only on template SHOW ??
|
||||
durationTime
|
||||
travelTime
|
||||
comment
|
||||
documents
|
||||
attendee
|
||||
#}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
{{ recordAction }}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
@ -1,3 +1,61 @@
|
||||
{% macro recordAction(activity, context = null, person_id = null, accompanying_course_id = null) %}
|
||||
{% if no_action is not defined or no_action == false %}
|
||||
<li>
|
||||
<a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', {
|
||||
'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity',
|
||||
'entityId': activity.id
|
||||
}) }}">{{ 'notification.Notify'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
|
||||
{#
|
||||
Disable person_id in following links, for redirect to accompanyingCourse context
|
||||
#}
|
||||
{% set person_id = null %}
|
||||
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_activity_activity_list',{
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-primary"
|
||||
title="{{ 'See activity in accompanying course context'|trans }}">
|
||||
<i class="fa fa-random fa-fw"></i>
|
||||
{{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_show', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-show"
|
||||
title="{{ 'Show'|trans }}"></a>
|
||||
</li>
|
||||
{% if no_action is not defined or no_action == false %}
|
||||
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_edit', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-update"
|
||||
title="{{ 'Edit'|trans }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_delete', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-delete"
|
||||
title="{{ 'Delete'|trans }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<div class="context-{{ context }}">
|
||||
|
||||
{% if activities|length == 0 %}
|
||||
@ -8,203 +66,10 @@
|
||||
{% else %}
|
||||
<div class="flex-table activity-list">
|
||||
{% for activity in activities %}
|
||||
{% set t = activity.type %}
|
||||
<div class="item-bloc">
|
||||
|
||||
<div class="item-row">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
{% if activity.date %}
|
||||
<p class="date-label">
|
||||
{{ activity.date|format_date('short') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
<h2 class="badge-title">
|
||||
<span class="title_label"></span>
|
||||
<span class="title_action">
|
||||
{{ activity.type.name | localize_translatable_string }}
|
||||
|
||||
{% if activity.emergency %}
|
||||
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-row column separator">
|
||||
<div class="wrap-list">
|
||||
{% if activity.location and t.locationVisible %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'location'|trans }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
<p class="wl-item">
|
||||
<span>{{ activity.location.locationType.title|localize_translatable_string }}</span>
|
||||
{{ activity.location.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if activity.sentReceived is not empty and t.sentReceivedVisible %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'Sent received'|trans }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
<p class="wl-item">
|
||||
{{ activity.sentReceived|capitalize|trans }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if activity.user and t.userVisible %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
<p class="wl-item">
|
||||
{{ activity.user.usernameCanonical|chill_entity_render_string|capitalize }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
|
||||
'context': context,
|
||||
'with_display': 'wrap-list',
|
||||
'entity': activity,
|
||||
'badge_person': true
|
||||
} %}
|
||||
|
||||
<div class="wrap-list">
|
||||
{%- if activity.reasons is not empty and t.reasonsVisible -%}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<h3>{{ 'Reasons'|trans }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% for r in activity.reasons %}
|
||||
<p class="wl-item reasons">
|
||||
{{ r|chill_entity_render_box }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{%- if activity.socialIssues is not empty and t.socialIssuesVisible -%}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<h3>{{ 'Social issues'|trans }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% for r in activity.socialIssues %}
|
||||
<p class="wl-item social-issues">
|
||||
{{ r|chill_entity_render_box }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{%- if activity.socialActions is not empty and t.socialActionsVisible -%}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<h3>{{ 'Social actions'|trans }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% for r in activity.socialActions %}
|
||||
<p class="wl-item social-actions">
|
||||
{{ r|chill_entity_render_box }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if activity.comment.comment is not empty and is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<h3>{{ 'Comment'|trans }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{{ activity.comment|chill_entity_render_box({
|
||||
'disable_markdown': false,
|
||||
'limit_lines': 3,
|
||||
'metadata': false
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Only if ACL SEE_DETAILS AND/OR only on template SHOW ??
|
||||
durationTime
|
||||
travelTime
|
||||
comment
|
||||
documents
|
||||
attendee
|
||||
#}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
|
||||
{#
|
||||
Disable person_id in following links, for redirect to accompanyingCourse context
|
||||
#}
|
||||
{% set person_id = null %}
|
||||
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_activity_activity_list',{
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-primary"
|
||||
title="{{ 'See activity in accompanying course context'|trans }}">
|
||||
<i class="fa fa-random fa-fw"></i>
|
||||
{{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_show', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-show"
|
||||
title="{{ 'Show'|trans }}"></a>
|
||||
</li>
|
||||
{% if no_action is not defined or no_action == false %}
|
||||
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_edit', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-update"
|
||||
title="{{ 'Edit'|trans }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_delete', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-delete"
|
||||
title="{{ 'Delete'|trans }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% include 'ChillActivityBundle:Activity:_list_item.html.twig' with {
|
||||
'context': context,
|
||||
'recordAction': _self.recordAction(activity, context, person_id, accompanying_course_id)
|
||||
} %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -4,6 +4,17 @@
|
||||
|
||||
{% block title %}{{ 'Activity list' |trans }}{% endblock title %}
|
||||
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% set person_id = null %}
|
||||
|
@ -20,6 +20,16 @@
|
||||
|
||||
{% block title %}{{ 'Activity list' |trans }}{% endblock title %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block personcontent %}
|
||||
|
||||
{% set person_id = null %}
|
||||
|
@ -198,8 +198,8 @@
|
||||
</a>
|
||||
</li>
|
||||
{% if is_granted('CHILL_ACTIVITY_UPDATE', entity) %}
|
||||
<li>
|
||||
<a class="btn btn-update" href="{{ path('chill_activity_activity_edit', { 'id': entity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}">
|
||||
<li>
|
||||
<a class="btn btn-update" href="{{ path('chill_activity_activity_edit', { 'id': entity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}">
|
||||
{{ 'Edit'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
@ -212,9 +212,3 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<script>
|
||||
import ShowPane from "../../../../ChillMainBundle/Resources/public/vuejs/Address/components/ShowPane";
|
||||
export default {
|
||||
components: {ShowPane}
|
||||
}
|
||||
</script>
|
||||
|
@ -4,6 +4,16 @@
|
||||
|
||||
{% block title 'Show the activity'|trans %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
|
||||
|
||||
{% block content -%}
|
||||
@ -11,3 +21,21 @@
|
||||
{% include 'ChillActivityBundle:Activity:show.html.twig' with {'context': 'accompanyingCourse'} %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block block_post_menu %}
|
||||
<div class="post-menu pt-4">
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a class="btn btn-primary" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity', 'entityId': entity.id}) }}">
|
||||
<i class="fa fa-paper-plane fa-fw"></i>
|
||||
{{ 'notification.Notify'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% set notifications = chill_list_notifications('Chill\\ActivityBundle\\Entity\\Activity', entity.id) %}
|
||||
{% if notifications is not empty %}
|
||||
{{ notifications|raw }}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,2 +1,27 @@
|
||||
{% macro recordAction(activity) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_show', {'id': activity.id }) }}"
|
||||
class="btn btn-show" title="{{ 'Show the activity'|trans }}"></a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
<a href="{{ path('chill_activity_activity_show', {'id': notification.relatedEntityId }) }}">Go to Activity</a>
|
||||
{% if activity is not null %}
|
||||
<div class="flex-table">
|
||||
{% if is_granted('CHILL_ACTIVITY_SEE', activity) %}
|
||||
{% include 'ChillActivityBundle:Activity:_list_item.html.twig' with {
|
||||
'recordAction': _self.recordAction(activity),
|
||||
'context': 'accompanyingCourse',
|
||||
'itemBlocClass': 'bg-chill-llight-gray'
|
||||
} %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning border-warning border-1">
|
||||
{{ 'This is the minimal activity data'|trans ~ ': ' ~ activity.id }}<br>
|
||||
{{ 'you are not allowed to see it details'|trans }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning border-warning border-1">
|
||||
{{ 'You get notified of an activity which does not exists any more'|trans }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -4,6 +4,16 @@
|
||||
|
||||
{% block title 'Show the activity'|trans %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
|
||||
|
||||
{% block personcontent -%}
|
||||
@ -11,3 +21,21 @@
|
||||
{% include 'ChillActivityBundle:Activity:show.html.twig' with {'context': 'person'} %}
|
||||
</div>
|
||||
{% endblock personcontent %}
|
||||
|
||||
{% block block_post_menu %}
|
||||
<div class="post-menu pt-4">
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a class="btn btn-primary" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity', 'entityId': entity.id}) }}">
|
||||
<i class="fa fa-paper-plane fa-fw"></i>
|
||||
{{ 'notification.Notify'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% set notifications = chill_list_notifications('Chill\\ActivityBundle\\Entity\\Activity', entity.id) %}
|
||||
{% if notifications is not empty %}
|
||||
{{ notifications|raw }}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -224,3 +224,7 @@ Aggregate by activity reason: Aggréger par sujet de l'activité
|
||||
Last activities: Les dernières activités
|
||||
|
||||
See activity in accompanying course context: Voir l'activité dans le contexte du parcours d'accompagnement
|
||||
|
||||
You get notified of an activity which does not exists any more: Cette notification ne correspond pas à une activité valide.
|
||||
you are not allowed to see it details: La notification fait référence à une activité à laquelle vous n'avez pas accès.
|
||||
This is the minimal activity data: Activité n°
|
||||
|
@ -26,7 +26,7 @@ class CollectionDocGenNormalizer implements ContextAwareNormalizerInterface, Nor
|
||||
|
||||
/**
|
||||
* @param Collection $object
|
||||
* @param null|string $format
|
||||
* @param string|null $format
|
||||
*
|
||||
* @return array|ArrayObject|bool|float|int|string|void|null
|
||||
*/
|
||||
|
@ -66,7 +66,7 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
|
||||
if (!$this->classMetadataFactory->hasMetadataFor($classMetadataKey)) {
|
||||
throw new LogicException(sprintf(
|
||||
'This object does not have metadata: %s. Add groups on this entity to allow to serialize with the format %s and groups %s',
|
||||
is_object($object) ? get_class($object) : '(todo' /*$context['docgen:expects'],*/,
|
||||
is_object($object) ? get_class($object) : '(todo' /*$context['docgen:expects'],*/ ,
|
||||
$format,
|
||||
implode(', ', ($context['groups'] ?? []))
|
||||
));
|
||||
|
@ -22,6 +22,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\RoleProvidersCompilerPass;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Chill\MainBundle\Search\SearchApiInterface;
|
||||
use Chill\MainBundle\Security\ProvideRoleInterface;
|
||||
@ -29,6 +30,7 @@ use Chill\MainBundle\Security\Resolver\CenterResolverInterface;
|
||||
use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
|
||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
|
||||
use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass;
|
||||
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
@ -50,6 +52,10 @@ class ChillMainBundle extends Bundle
|
||||
->addTag('chill.render_entity');
|
||||
$container->registerForAutoconfiguration(SearchApiInterface::class)
|
||||
->addTag('chill.search_api_provider');
|
||||
$container->registerForAutoconfiguration(NotificationHandlerInterface::class)
|
||||
->addTag('chill_main.notification_handler');
|
||||
$container->registerForAutoconfiguration(NotificationCounterInterface::class)
|
||||
->addTag('chill.count_notification.user');
|
||||
|
||||
$container->addCompilerPass(new SearchableServicesCompilerPass());
|
||||
$container->addCompilerPass(new ConfigConsistencyCompilerPass());
|
||||
|
@ -0,0 +1,87 @@
|
||||
<?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\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Security\Authorization\NotificationVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* @Route("/api/1.0/main/notification")
|
||||
*/
|
||||
class NotificationApiController
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, Security $security)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/mark/read", name="chill_api_main_notification_mark_read", methods={"POST"})
|
||||
*/
|
||||
public function markAsRead(Notification $notification): JsonResponse
|
||||
{
|
||||
return $this->markAs('read', $notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/mark/unread", name="chill_api_main_notification_mark_unread", methods={"POST"})
|
||||
*/
|
||||
public function markAsUnread(Notification $notification): JsonResponse
|
||||
{
|
||||
return $this->markAs('unread', $notification);
|
||||
}
|
||||
|
||||
private function markAs(string $target, Notification $notification): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(NotificationVoter::NOTIFICATION_TOGGLE_READ_STATUS, $notification)) {
|
||||
throw new AccessDeniedException('Not allowed to toggle read status of notification');
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new RuntimeException('not possible to mark as read by this user');
|
||||
}
|
||||
|
||||
switch ($target) {
|
||||
case 'read':
|
||||
$notification->markAsReadBy($user);
|
||||
|
||||
break;
|
||||
|
||||
case 'unread':
|
||||
$notification->markAsUnreadBy($user);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnexpectedValueException("target not supported: {$target}");
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(null, JsonResponse::HTTP_ACCEPTED, [], false);
|
||||
}
|
||||
}
|
@ -11,59 +11,292 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Notification\NotificationRenderer;
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\NotificationComment;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\NotificationCommentType;
|
||||
use Chill\MainBundle\Form\NotificationType;
|
||||
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerManager;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
use Chill\MainBundle\Security\Authorization\NotificationVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
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 Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @Route("/{_locale}/notification")
|
||||
*/
|
||||
class NotificationController extends AbstractController
|
||||
{
|
||||
private $security;
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
public function __construct(Security $security)
|
||||
{
|
||||
private NotificationHandlerManager $notificationHandlerManager;
|
||||
|
||||
private NotificationRepository $notificationRepository;
|
||||
|
||||
private PaginatorFactory $paginatorFactory;
|
||||
|
||||
private Security $security;
|
||||
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
Security $security,
|
||||
NotificationRepository $notificationRepository,
|
||||
NotificationHandlerManager $notificationHandlerManager,
|
||||
PaginatorFactory $paginatorFactory,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->security = $security;
|
||||
$this->notificationRepository = $notificationRepository;
|
||||
$this->notificationHandlerManager = $notificationHandlerManager;
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/show", name="chill_main_notification_show")
|
||||
* @Route("/create", name="chill_main_notification_create")
|
||||
*/
|
||||
public function showAction(
|
||||
NotificationRepository $notificationRepository,
|
||||
NotificationRenderer $notificationRenderer,
|
||||
PaginatorFactory $paginatorFactory
|
||||
) {
|
||||
public function createAction(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||
|
||||
if (!$this->security->getUser() instanceof User) {
|
||||
throw new AccessDeniedHttpException('You must be authenticated and a user to create a notification');
|
||||
}
|
||||
|
||||
if (!$request->query->has('entityClass')) {
|
||||
throw new BadRequestHttpException('Missing entityClass parameter');
|
||||
}
|
||||
|
||||
if (!$request->query->has('entityId')) {
|
||||
throw new BadRequestHttpException('missing entityId parameter');
|
||||
}
|
||||
|
||||
$notification = new Notification();
|
||||
$notification
|
||||
->setRelatedEntityClass($request->query->get('entityClass'))
|
||||
->setRelatedEntityId($request->query->getInt('entityId'))
|
||||
->setSender($this->security->getUser());
|
||||
|
||||
try {
|
||||
$handler = $this->notificationHandlerManager->getHandler($notification);
|
||||
} catch (NotificationHandlerNotFound $e) {
|
||||
throw new BadRequestHttpException('no handler for this notification');
|
||||
}
|
||||
|
||||
$form = $this->createForm(NotificationType::class, $notification);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->em->persist($notification);
|
||||
$this->em->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator->trans('notification.Notification created'));
|
||||
|
||||
if ($request->query->has('returnPath')) {
|
||||
return new RedirectResponse($request->query->get('returnPath'));
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('chill_main_homepage');
|
||||
}
|
||||
|
||||
return $this->render('@ChillMain/Notification/create.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'handler' => $handler,
|
||||
'notification' => $notification,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit", name="chill_main_notification_edit")
|
||||
*/
|
||||
public function editAction(Notification $notification, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(NotificationVoter::NOTIFICATION_UPDATE, $notification);
|
||||
|
||||
$form = $this->createForm(NotificationType::class, $notification);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->em->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator->trans('notification.Notification updated'));
|
||||
|
||||
if ($request->query->has('returnPath')) {
|
||||
return new RedirectResponse($request->query->get('returnPath'));
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('chill_main_notification_my');
|
||||
}
|
||||
|
||||
return $this->render('@ChillMain/Notification/edit.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'handler' => $this->notificationHandlerManager->getHandler($notification),
|
||||
'notification' => $notification,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/inbox", name="chill_main_notification_my")
|
||||
*/
|
||||
public function inboxAction(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||
$currentUser = $this->security->getUser();
|
||||
|
||||
$notificationsNbr = $notificationRepository->countAllForAttendee(($currentUser));
|
||||
$paginator = $paginatorFactory->create($notificationsNbr);
|
||||
$notificationsNbr = $this->notificationRepository->countAllForAttendee(($currentUser));
|
||||
$paginator = $this->paginatorFactory->create($notificationsNbr);
|
||||
|
||||
$notifications = $notificationRepository->findAllForAttendee(
|
||||
$notifications = $this->notificationRepository->findAllForAttendee(
|
||||
$currentUser,
|
||||
$limit = $paginator->getItemsPerPage(),
|
||||
$offset = $paginator->getCurrentPage()->getFirstItemNumber()
|
||||
);
|
||||
|
||||
return $this->render('@ChillMain/Notification/list.html.twig', [
|
||||
'datas' => $this->itemsForTemplate($notifications),
|
||||
'notifications' => $notifications,
|
||||
'paginator' => $paginator,
|
||||
'step' => 'inbox',
|
||||
'unreads' => $this->countUnread(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/sent", name="chill_main_notification_sent")
|
||||
*/
|
||||
public function sentAction(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||
$currentUser = $this->security->getUser();
|
||||
|
||||
$notificationsNbr = $this->notificationRepository->countAllForSender($currentUser);
|
||||
$paginator = $this->paginatorFactory->create($notificationsNbr);
|
||||
|
||||
$notifications = $this->notificationRepository->findAllForSender(
|
||||
$currentUser,
|
||||
$limit = $paginator->getItemsPerPage(),
|
||||
$offset = $paginator->getCurrentPage()->getFirstItemNumber()
|
||||
);
|
||||
|
||||
return $this->render('@ChillMain/Notification/list.html.twig', [
|
||||
'datas' => $this->itemsForTemplate($notifications),
|
||||
'notifications' => $notifications,
|
||||
'paginator' => $paginator,
|
||||
'step' => 'sent',
|
||||
'unreads' => $this->countUnread(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/show", name="chill_main_notification_show")
|
||||
*/
|
||||
public function showAction(Notification $notification, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(NotificationVoter::NOTIFICATION_SEE, $notification);
|
||||
|
||||
if ($request->query->has('edit')) {
|
||||
$commentId = $request->query->getInt('edit');
|
||||
$editedComment = $notification->getComments()->filter(static function (NotificationComment $c) use ($commentId) {
|
||||
return $c->getId() === $commentId;
|
||||
})->first();
|
||||
|
||||
if (false === $editedComment) {
|
||||
throw $this->createNotFoundException("Comment with id {$commentId} does not exists nor belong to this notification");
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(NotificationVoter::COMMENT_EDIT, $editedComment);
|
||||
|
||||
$editedCommentForm = $this->createForm(NotificationCommentType::class, $editedComment);
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod() && 'edit' === $request->request->get('form')) {
|
||||
$editedCommentForm->handleRequest($request);
|
||||
|
||||
if ($editedCommentForm->isSubmitted() && $editedCommentForm->isValid()) {
|
||||
$this->em->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator->trans('notification.comment_updated'));
|
||||
|
||||
return $this->redirectToRoute('chill_main_notification_show', [
|
||||
'id' => $notification->getId(),
|
||||
'_fragment' => 'comment-' . $commentId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isGranted(NotificationVoter::COMMENT_ADD, $notification)) {
|
||||
$appendComment = new NotificationComment();
|
||||
$appendCommentForm = $this->createForm(NotificationCommentType::class, $appendComment);
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod() && 'append' === $request->request->get('form')) {
|
||||
$appendCommentForm->handleRequest($request);
|
||||
|
||||
if ($appendCommentForm->isSubmitted() && $appendCommentForm->isValid()) {
|
||||
$notification->addComment($appendComment);
|
||||
$this->em->persist($appendComment);
|
||||
$this->em->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator->trans('notification.comment_appended'));
|
||||
|
||||
return $this->redirectToRoute('chill_main_notification_show', [
|
||||
'id' => $notification->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response = $this->render('@ChillMain/Notification/show.html.twig', [
|
||||
'notification' => $notification,
|
||||
'handler' => $this->notificationHandlerManager->getHandler($notification),
|
||||
'appendCommentForm' => isset($appendCommentForm) ? $appendCommentForm->createView() : null,
|
||||
'editedCommentForm' => isset($editedCommentForm) ? $editedCommentForm->createView() : null,
|
||||
'editedCommentId' => $commentId ?? null,
|
||||
]);
|
||||
|
||||
// we mark the notification as read after having computed the response
|
||||
if ($this->getUser() instanceof User && !$notification->isReadBy($this->getUser())) {
|
||||
$notification->markAsReadBy($this->getUser());
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function countUnread(): array
|
||||
{
|
||||
return [
|
||||
'sent' => $this->notificationRepository->countUnreadByUserWhereSender($this->security->getUser()),
|
||||
'inbox' => $this->notificationRepository->countUnreadByUserWhereAddressee($this->security->getUser()),
|
||||
];
|
||||
}
|
||||
|
||||
private function itemsForTemplate(array $notifications): array
|
||||
{
|
||||
$templateData = [];
|
||||
|
||||
foreach ($notifications as $notification) {
|
||||
$data = [
|
||||
'template' => $notificationRenderer->getTemplate($notification),
|
||||
'template_data' => $notificationRenderer->getTemplateData($notification),
|
||||
$templateData[] = [
|
||||
'template' => $this->notificationHandlerManager->getTemplate($notification),
|
||||
'template_data' => $this->notificationHandlerManager->getTemplateData($notification),
|
||||
'notification' => $notification,
|
||||
];
|
||||
$templateData[] = $data;
|
||||
}
|
||||
|
||||
return $this->render('@ChillMain/Notification/show.html.twig', [
|
||||
'datas' => $templateData,
|
||||
'notifications' => $notifications,
|
||||
'paginator' => $paginator,
|
||||
]);
|
||||
return $templateData;
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Entity;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@ -20,19 +22,27 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(
|
||||
* name="chill_main_notification",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"relatedEntityClass", "relatedEntityId"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
*/
|
||||
class Notification
|
||||
class Notification implements TrackUpdateInterface
|
||||
{
|
||||
private array $addedAddresses = [];
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity=User::class)
|
||||
* @ORM\JoinTable(name="chill_main_notification_addresses_user")
|
||||
*/
|
||||
private Collection $addressees;
|
||||
|
||||
private ?ArrayCollection $addressesOnLoad = null;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity=NotificationComment::class, mappedBy="notification", orphanRemoval=true)
|
||||
* @ORM\OrderBy({"createdAt": "ASC"})
|
||||
*/
|
||||
private Collection $comments;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable")
|
||||
*/
|
||||
@ -43,43 +53,84 @@ class Notification
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private int $id;
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text")
|
||||
*/
|
||||
private string $message;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json")
|
||||
*/
|
||||
private array $read;
|
||||
private string $message = '';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255)
|
||||
*/
|
||||
private string $relatedEntityClass;
|
||||
private string $relatedEntityClass = '';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private int $relatedEntityId;
|
||||
|
||||
private array $removedAddresses = [];
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class)
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private User $sender;
|
||||
private ?User $sender = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", options={"default": ""})
|
||||
*/
|
||||
private string $title = '';
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity=User::class)
|
||||
* @ORM\JoinTable(name="chill_main_notification_addresses_unread")
|
||||
*/
|
||||
private Collection $unreadBy;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable")
|
||||
*/
|
||||
private ?DateTimeImmutable $updatedAt;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class)
|
||||
*/
|
||||
private ?User $updatedBy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->addressees = new ArrayCollection();
|
||||
$this->unreadBy = new ArrayCollection();
|
||||
$this->comments = new ArrayCollection();
|
||||
$this->setDate(new DateTimeImmutable());
|
||||
}
|
||||
|
||||
public function addAddressee(User $addressee): self
|
||||
{
|
||||
if (!$this->addressees->contains($addressee)) {
|
||||
$this->addressees[] = $addressee;
|
||||
$this->addedAddresses[] = $addressee;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addComment(NotificationComment $comment): self
|
||||
{
|
||||
if (!$this->comments->contains($comment)) {
|
||||
$this->comments[] = $comment;
|
||||
$comment->setNotification($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addUnreadBy(User $user): self
|
||||
{
|
||||
if (!$this->unreadBy->contains($user)) {
|
||||
$this->unreadBy[] = $user;
|
||||
}
|
||||
|
||||
return $this;
|
||||
@ -90,9 +141,19 @@ class Notification
|
||||
*/
|
||||
public function getAddressees(): Collection
|
||||
{
|
||||
// keep a copy to compute changes later
|
||||
if (null === $this->addressesOnLoad) {
|
||||
$this->addressesOnLoad = new ArrayCollection($this->addressees->toArray());
|
||||
}
|
||||
|
||||
return $this->addressees;
|
||||
}
|
||||
|
||||
public function getComments(): Collection
|
||||
{
|
||||
return $this->comments;
|
||||
}
|
||||
|
||||
public function getDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->date;
|
||||
@ -108,11 +169,6 @@ class Notification
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function getRead(): array
|
||||
{
|
||||
return $this->read;
|
||||
}
|
||||
|
||||
public function getRelatedEntityClass(): ?string
|
||||
{
|
||||
return $this->relatedEntityClass;
|
||||
@ -128,9 +184,97 @@ class Notification
|
||||
return $this->sender;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getUnreadBy(): Collection
|
||||
{
|
||||
return $this->unreadBy;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function getUpdatedBy(): ?User
|
||||
{
|
||||
return $this->updatedBy;
|
||||
}
|
||||
|
||||
public function isReadBy(User $user): bool
|
||||
{
|
||||
return !$this->unreadBy->contains($user);
|
||||
}
|
||||
|
||||
public function isSystem(): bool
|
||||
{
|
||||
return null === $this->sender;
|
||||
}
|
||||
|
||||
public function markAsReadBy(User $user): self
|
||||
{
|
||||
return $this->removeUnreadBy($user);
|
||||
}
|
||||
|
||||
public function markAsUnreadBy(User $user): self
|
||||
{
|
||||
return $this->addUnreadBy($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\PreFlush
|
||||
*/
|
||||
public function registerUnread()
|
||||
{
|
||||
foreach ($this->addedAddresses as $addressee) {
|
||||
$this->addUnreadBy($addressee);
|
||||
}
|
||||
|
||||
foreach ($this->removedAddresses as $addressee) {
|
||||
$this->removeAddressee($addressee);
|
||||
}
|
||||
|
||||
if (null !== $this->addressesOnLoad) {
|
||||
foreach ($this->addressees as $existingAddresse) {
|
||||
if (!$this->addressesOnLoad->contains($existingAddresse)) {
|
||||
$this->addUnreadBy($existingAddresse);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->addressesOnLoad as $onLoadAddressee) {
|
||||
if (!$this->addressees->contains($onLoadAddressee)) {
|
||||
$this->removeUnreadBy($onLoadAddressee);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->removedAddresses = [];
|
||||
$this->addedAddresses = [];
|
||||
$this->addressesOnLoad = null;
|
||||
}
|
||||
|
||||
public function removeAddressee(User $addressee): self
|
||||
{
|
||||
$this->addressees->removeElement($addressee);
|
||||
if ($this->addressees->removeElement($addressee)) {
|
||||
$this->removedAddresses[] = $addressee;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeComment(NotificationComment $comment): self
|
||||
{
|
||||
$this->comments->removeElement($comment);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeUnreadBy(User $user): self
|
||||
{
|
||||
$this->unreadBy->removeElement($user);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -149,13 +293,6 @@ class Notification
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRead(array $read): self
|
||||
{
|
||||
$this->read = $read;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRelatedEntityClass(string $relatedEntityClass): self
|
||||
{
|
||||
$this->relatedEntityClass = $relatedEntityClass;
|
||||
@ -176,4 +313,25 @@ class Notification
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): Notification
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(DateTimeInterface $datetime): self
|
||||
{
|
||||
$this->updatedAt = $datetime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUpdatedBy(User $user): self
|
||||
{
|
||||
$this->updatedBy = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
191
src/Bundle/ChillMainBundle/Entity/NotificationComment.php
Normal file
191
src/Bundle/ChillMainBundle/Entity/NotificationComment.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?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\MainBundle\Entity;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
use Doctrine\ORM\Event\PreFlushEventArgs;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table("chill_main_notification_comment")
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
*/
|
||||
class NotificationComment implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="text")
|
||||
*/
|
||||
private string $content = '';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=true)
|
||||
*/
|
||||
private ?DateTimeImmutable $createdAt = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class)
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private ?User $createdBy = null;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=Notification::class, inversedBy="comments")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*/
|
||||
private ?Notification $notification = null;
|
||||
|
||||
/**
|
||||
* Internal variable which detect if the comment is just persisted.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
private bool $recentlyPersisted = false;
|
||||
|
||||
/**
|
||||
* TODO typo in property (hotfixed).
|
||||
*
|
||||
* @ORM\Column(type="datetime_immutable", nullable=true)
|
||||
*/
|
||||
private ?DateTimeImmutable $updateAt = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class)
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private ?User $updatedBy = null;
|
||||
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getCreatedBy(): ?User
|
||||
{
|
||||
return $this->createdBy;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getNotification(): ?Notification
|
||||
{
|
||||
return $this->notification;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->updateAt;
|
||||
}
|
||||
|
||||
public function getUpdatedBy(): ?User
|
||||
{
|
||||
return $this->updatedBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\PreFlush
|
||||
*/
|
||||
public function onFlushMarkNotificationAsUnread(PreFlushEventArgs $eventArgs): void
|
||||
{
|
||||
if ($this->recentlyPersisted) {
|
||||
foreach ($this->getNotification()->getAddressees() as $addressee) {
|
||||
if ($this->getCreatedBy() !== $addressee) {
|
||||
$this->getNotification()->markAsUnreadBy($addressee);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->getNotification()->getSender() !== $this->getCreatedBy()) {
|
||||
$this->getNotification()->markAsUnreadBy($this->getNotification()->getSender());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\PrePersist
|
||||
*/
|
||||
public function onPrePersist(LifecycleEventArgs $eventArgs): void
|
||||
{
|
||||
$this->recentlyPersisted = true;
|
||||
}
|
||||
|
||||
public function setContent(string $content): self
|
||||
{
|
||||
$this->content = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCreatedAt(DateTimeInterface $datetime): self
|
||||
{
|
||||
$this->createdAt = $datetime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCreatedBy(User $user): self
|
||||
{
|
||||
$this->createdBy = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal use Notification::addComment
|
||||
*/
|
||||
public function setNotification(?Notification $notification): self
|
||||
{
|
||||
$this->notification = $notification;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use @see{self::setUpdatedAt} instead
|
||||
*/
|
||||
public function setUpdateAt(?DateTimeImmutable $updateAt): self
|
||||
{
|
||||
return $this->setUpdatedAt($updateAt);
|
||||
}
|
||||
|
||||
public function setUpdatedAt(DateTimeInterface $datetime): self
|
||||
{
|
||||
$this->updateAt = $datetime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUpdatedBy(User $user): self
|
||||
{
|
||||
$this->updatedBy = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
26
src/Bundle/ChillMainBundle/Form/NotificationCommentType.php
Normal file
26
src/Bundle/ChillMainBundle/Form/NotificationCommentType.php
Normal 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\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class NotificationCommentType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('content', ChillTextareaType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
}
|
43
src/Bundle/ChillMainBundle/Form/NotificationType.php
Normal file
43
src/Bundle/ChillMainBundle/Form/NotificationType.php
Normal 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\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class NotificationType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('title', TextType::class, [
|
||||
'label' => 'Title',
|
||||
'required' => true,
|
||||
])
|
||||
->add('addressees', PickUserDynamicType::class, [
|
||||
'multiple' => true,
|
||||
])
|
||||
->add('message', ChillTextareaType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefault('class', Notification::class);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
<?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\MainBundle\Form\Type\DataTransformer;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use function array_key_exists;
|
||||
|
||||
class UserToJsonTransformer implements DataTransformerInterface
|
||||
{
|
||||
private DenormalizerInterface $denormalizer;
|
||||
|
||||
private bool $multiple;
|
||||
|
||||
private SerializerInterface $serializer;
|
||||
|
||||
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer, bool $multiple)
|
||||
{
|
||||
$this->denormalizer = $denormalizer;
|
||||
$this->serializer = $serializer;
|
||||
$this->multiple = $multiple;
|
||||
}
|
||||
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if ($this->multiple) {
|
||||
return array_map(
|
||||
function ($item) { return $this->denormalizeOne($item); },
|
||||
json_decode($value, true)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->denormalizeOne(json_decode($value, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User|User[] $value
|
||||
*/
|
||||
public function transform($value): string
|
||||
{
|
||||
if (null === $value) {
|
||||
return $this->multiple ? 'null' : '[]';
|
||||
}
|
||||
|
||||
return $this->serializer->serialize($value, 'json', [
|
||||
AbstractNormalizer::GROUPS => ['read'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function denormalizeOne(array $item): User
|
||||
{
|
||||
if (!array_key_exists('type', $item)) {
|
||||
throw new TransformationFailedException('the key "type" is missing on element');
|
||||
}
|
||||
|
||||
if (!array_key_exists('id', $item)) {
|
||||
throw new TransformationFailedException('the key "id" is missing on element');
|
||||
}
|
||||
|
||||
return
|
||||
$this->denormalizer->denormalize(
|
||||
['type' => $item['type'], 'id' => $item['id']],
|
||||
User::class,
|
||||
'json',
|
||||
[AbstractNormalizer::GROUPS => ['read']],
|
||||
);
|
||||
}
|
||||
}
|
63
src/Bundle/ChillMainBundle/Form/Type/PickUserDynamicType.php
Normal file
63
src/Bundle/ChillMainBundle/Form/Type/PickUserDynamicType.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?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\MainBundle\Form\Type;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\DataTransformer\UserToJsonTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Pick user dymically, using vuejs module "AddPerson".
|
||||
*/
|
||||
class PickUserDynamicType extends AbstractType
|
||||
{
|
||||
private DenormalizerInterface $denormalizer;
|
||||
|
||||
private SerializerInterface $serializer;
|
||||
|
||||
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer)
|
||||
{
|
||||
$this->denormalizer = $denormalizer;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->addViewTransformer(new UserToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple']));
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['multiple'] = $options['multiple'];
|
||||
$view->vars['types'] = ['user'];
|
||||
$view->vars['uniqid'] = uniqid('pick_user_dyn');
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setDefault('multiple', false)
|
||||
->setAllowedTypes('multiple', ['bool'])
|
||||
->setDefault('compound', false);
|
||||
}
|
||||
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'pick_user_dynamic';
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
<?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\MainBundle\Notification\Counter;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\NotificationComment;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
|
||||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
use Doctrine\ORM\Event\PreFlushEventArgs;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
final class NotificationByUserCounter implements NotificationCounterInterface
|
||||
{
|
||||
private CacheItemPoolInterface $cacheItemPool;
|
||||
|
||||
private NotificationRepository $notificationRepository;
|
||||
|
||||
public function __construct(CacheItemPoolInterface $cacheItemPool, NotificationRepository $notificationRepository)
|
||||
{
|
||||
$this->cacheItemPool = $cacheItemPool;
|
||||
$this->notificationRepository = $notificationRepository;
|
||||
}
|
||||
|
||||
public function addNotification(UserInterface $u): int
|
||||
{
|
||||
if (!$u instanceof User) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->countUnreadByUser($u);
|
||||
}
|
||||
|
||||
public function countUnreadByUser(User $user): int
|
||||
{
|
||||
$key = self::generateCacheKeyUnreadNotificationByUser($user);
|
||||
|
||||
$item = $this->cacheItemPool->getItem($key);
|
||||
|
||||
if ($item->isHit()) {
|
||||
return $item->get();
|
||||
}
|
||||
|
||||
$unreads = $this->notificationRepository->countUnreadByUser($user);
|
||||
|
||||
$item
|
||||
->set($unreads)
|
||||
// keep in cache for 15 minutes
|
||||
->expiresAfter(60 * 15);
|
||||
$this->cacheItemPool->save($item);
|
||||
|
||||
return $unreads;
|
||||
}
|
||||
|
||||
public static function generateCacheKeyUnreadNotificationByUser(User $user): string
|
||||
{
|
||||
return 'chill_main_notif_unread_by_' . $user->getId();
|
||||
}
|
||||
|
||||
public function onEditNotificationComment(NotificationComment $notificationComment, LifecycleEventArgs $eventArgs): void
|
||||
{
|
||||
$this->resetCacheForNotification($notificationComment->getNotification());
|
||||
}
|
||||
|
||||
public function onPreFlushNotification(Notification $notification, PreFlushEventArgs $eventArgs): void
|
||||
{
|
||||
$this->resetCacheForNotification($notification);
|
||||
}
|
||||
|
||||
private function resetCacheForNotification(Notification $notification): void
|
||||
{
|
||||
$keys = [];
|
||||
|
||||
if (null !== $notification->getSender()) {
|
||||
$keys[] = self::generateCacheKeyUnreadNotificationByUser($notification->getSender());
|
||||
}
|
||||
|
||||
foreach ($notification->getAddressees() as $addressee) {
|
||||
$keys[] = self::generateCacheKeyUnreadNotificationByUser($addressee);
|
||||
}
|
||||
|
||||
$this->cacheItemPool->deleteItems($keys);
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
<?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\MainBundle\Notification\Email;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\NotificationComment;
|
||||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class NotificationMailer
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private MailerInterface $mailer;
|
||||
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
public function __construct(MailerInterface $mailer, LoggerInterface $logger, TranslatorInterface $translator)
|
||||
{
|
||||
$this->mailer = $mailer;
|
||||
$this->logger = $logger;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
public function postPersistComment(NotificationComment $comment, LifecycleEventArgs $eventArgs): void
|
||||
{
|
||||
foreach (
|
||||
array_merge(
|
||||
$comment->getNotification()->getAddressees()->toArray(),
|
||||
[$comment->getNotification()->getSender()]
|
||||
) as $dest
|
||||
) {
|
||||
if (null === $dest->getEmail() || $comment->getCreatedBy() !== $dest) {
|
||||
continue;
|
||||
}
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->to($dest->getEmail())
|
||||
->subject('Re: [Chill] ' . $comment->getNotification()->getTitle())
|
||||
->textTemplate('@ChillMain/Notification/email_notification_comment_persist.fr.md.twig')
|
||||
->context([
|
||||
'comment' => $comment,
|
||||
'dest' => $dest,
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->mailer->send($email);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->warning('[NotificationMailer] could not send an email notification about comment', [
|
||||
'to' => $dest->getEmail(),
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a email after a notification is persisted.
|
||||
*/
|
||||
public function postPersistNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
|
||||
{
|
||||
foreach ($notification->getAddressees() as $addressee) {
|
||||
if (null === $addressee->getEmail()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($notification->isSystem()) {
|
||||
$email = new Email();
|
||||
$email
|
||||
->text($notification->getMessage())
|
||||
->subject('[Chill] ' . $notification->getTitle());
|
||||
} else {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
|
||||
->context([
|
||||
'notification' => $notification,
|
||||
'dest' => $addressee,
|
||||
]);
|
||||
}
|
||||
|
||||
$email->to($addressee->getEmail());
|
||||
|
||||
try {
|
||||
$this->mailer->send($email);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->warning('[NotificationMailer] could not send an email notification', [
|
||||
'to' => $addressee->getEmail(),
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?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\MainBundle\Notification\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class NotificationHandlerNotFound extends RuntimeException
|
||||
{
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?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\MainBundle\Notification;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
|
||||
interface NotificationHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Return the template path (twig file).
|
||||
*/
|
||||
public function getTemplate(Notification $notification, array $options = []): string;
|
||||
|
||||
/**
|
||||
* Return an array which will be passed as data for the template.
|
||||
*/
|
||||
public function getTemplateData(Notification $notification, array $options = []): array;
|
||||
|
||||
/**
|
||||
* Return true if the handler supports the handling for this notification.
|
||||
*/
|
||||
public function supports(Notification $notification, array $options = []): bool;
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?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\MainBundle\Notification;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final class NotificationHandlerManager
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private iterable $handlers;
|
||||
|
||||
public function __construct(
|
||||
iterable $handlers,
|
||||
EntityManagerInterface $em
|
||||
) {
|
||||
$this->handlers = $handlers;
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throw NotificationHandlerNotFound if handler is not found
|
||||
*/
|
||||
public function getHandler(Notification $notification, array $options = []): NotificationHandlerInterface
|
||||
{
|
||||
foreach ($this->handlers as $renderer) {
|
||||
if ($renderer->supports($notification, $options)) {
|
||||
return $renderer;
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotificationHandlerNotFound();
|
||||
}
|
||||
|
||||
public function getTemplate(Notification $notification, array $options = []): string
|
||||
{
|
||||
return $this->getHandler($notification, $options)->getTemplate($notification, $options);
|
||||
}
|
||||
|
||||
public function getTemplateData(Notification $notification, array $options = []): array
|
||||
{
|
||||
return $this->getHandler($notification, $options)->getTemplateData($notification, $options);
|
||||
}
|
||||
}
|
@ -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\MainBundle\Notification;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* Helps to find if a notification exist for a given entity.
|
||||
*/
|
||||
class NotificationPresence
|
||||
{
|
||||
private NotificationRepository $notificationRepository;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(Security $security, NotificationRepository $notificationRepository)
|
||||
{
|
||||
$this->security = $security;
|
||||
$this->notificationRepository = $notificationRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Notification[]
|
||||
*/
|
||||
public function getNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId): array
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if ($user instanceof User) {
|
||||
return $this->notificationRepository->findNotificationByRelatedEntityAndUserAssociated(
|
||||
$relatedEntityClass,
|
||||
$relatedEntityId,
|
||||
$user
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
<?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\MainBundle\Notification;
|
||||
|
||||
use Chill\ActivityBundle\Notification\ActivityNotificationRenderer;
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\PersonBundle\Notification\AccompanyingPeriodNotificationRenderer;
|
||||
use Exception;
|
||||
|
||||
final class NotificationRenderer
|
||||
{
|
||||
private array $renderers;
|
||||
|
||||
public function __construct(
|
||||
AccompanyingPeriodNotificationRenderer $accompanyingPeriodNotificationRenderer,
|
||||
ActivityNotificationRenderer $activityNotificationRenderer
|
||||
) {
|
||||
// TODO configure automatically
|
||||
// TODO CREER UNE INTERFACE POUR ETRE SUR QUE LES RENDERERS SONT OK
|
||||
|
||||
$this->renderers[] = $accompanyingPeriodNotificationRenderer;
|
||||
$this->renderers[] = $activityNotificationRenderer;
|
||||
}
|
||||
|
||||
public function getTemplate(Notification $notification)
|
||||
{
|
||||
return $this->getRenderer($notification)->getTemplate();
|
||||
}
|
||||
|
||||
public function getTemplateData(Notification $notification)
|
||||
{
|
||||
return $this->getRenderer($notification)->getTemplateData($notification);
|
||||
}
|
||||
|
||||
private function getRenderer(Notification $notification)
|
||||
{
|
||||
foreach ($this->renderers as $renderer) {
|
||||
if ($renderer->supports($notification)) {
|
||||
return $renderer;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception('No renderer for ' . $notification);
|
||||
}
|
||||
}
|
@ -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\MainBundle\Notification\Templating;
|
||||
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class NotificationTwigExtension extends AbstractExtension
|
||||
{
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new TwigFunction('chill_list_notifications', [NotificationTwigExtensionRuntime::class, 'listNotificationsFor'], [
|
||||
'needs_environment' => true,
|
||||
'is_safe' => ['html'],
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
@ -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\MainBundle\Notification\Templating;
|
||||
|
||||
use Chill\MainBundle\Notification\NotificationPresence;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
private NotificationPresence $notificationPresence;
|
||||
|
||||
public function __construct(NotificationPresence $notificationPresence)
|
||||
{
|
||||
$this->notificationPresence = $notificationPresence;
|
||||
}
|
||||
|
||||
public function listNotificationsFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string
|
||||
{
|
||||
$notifications = $this->notificationPresence->getNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId);
|
||||
|
||||
if ([] === $notifications) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $environment->render('@ChillMain/Notification/extension_list_notifications_for.html.twig', [
|
||||
'notifications' => $notifications,
|
||||
]);
|
||||
}
|
||||
}
|
@ -13,25 +13,76 @@ namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
final class NotificationRepository implements ObjectRepository
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->em = $entityManager;
|
||||
$this->repository = $entityManager->getRepository(Notification::class);
|
||||
}
|
||||
|
||||
public function countAllForAttendee(User $addressee): int // TODO passer à attendees avec S
|
||||
public function countAllForAttendee(User $addressee): int
|
||||
{
|
||||
$query = $this->queryAllForAttendee($addressee, $countQuery = true);
|
||||
return $this->queryByAddressee($addressee)
|
||||
->select('count(n)')
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
return $query->getSingleScalarResult();
|
||||
public function countAllForSender(User $sender): int
|
||||
{
|
||||
return $this->queryBySender($sender)
|
||||
->select('count(n)')
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countUnreadByUser(User $user): int
|
||||
{
|
||||
$sql = 'SELECT count(*) AS c FROM chill_main_notification_addresses_unread WHERE user_id = :userId';
|
||||
|
||||
$rsm = new Query\ResultSetMapping();
|
||||
$rsm->addScalarResult('c', 'c', Types::INTEGER);
|
||||
|
||||
$nq = $this->em->createNativeQuery($sql, $rsm)
|
||||
->setParameter('userId', $user->getId());
|
||||
|
||||
return $nq->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countUnreadByUserWhereAddressee(User $user): int
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('n');
|
||||
$qb
|
||||
->select('count(n)')
|
||||
->where($qb->expr()->isMemberOf(':user', 'n.addressees'))
|
||||
->andWhere($qb->expr()->isMemberOf(':user', 'n.unreadBy'))
|
||||
->setParameter('user', $user);
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countUnreadByUserWhereSender(User $user): int
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('n');
|
||||
$qb
|
||||
->select('count(n)')
|
||||
->where($qb->expr()->eq('n.sender', ':user'))
|
||||
->andWhere($qb->expr()->isMemberOf(':user', 'n.unreadBy'))
|
||||
->setParameter('user', $user);
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function find($id, $lockMode = null, $lockVersion = null): ?Notification
|
||||
@ -53,9 +104,9 @@ final class NotificationRepository implements ObjectRepository
|
||||
*
|
||||
* @return Notification[]
|
||||
*/
|
||||
public function findAllForAttendee(User $addressee, $limit = null, $offset = null): array // TODO passer à attendees avec S
|
||||
public function findAllForAttendee(User $addressee, $limit = null, $offset = null): array
|
||||
{
|
||||
$query = $this->queryAllForAttendee($addressee);
|
||||
$query = $this->queryByAddressee($addressee)->select('n');
|
||||
|
||||
if ($limit) {
|
||||
$query = $query->setMaxResults($limit);
|
||||
@ -65,7 +116,26 @@ final class NotificationRepository implements ObjectRepository
|
||||
$query = $query->setFirstResult($offset);
|
||||
}
|
||||
|
||||
return $query->getResult();
|
||||
$query->addOrderBy('n.date', 'DESC');
|
||||
|
||||
return $query->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findAllForSender(User $sender, $limit = null, $offset = null): array
|
||||
{
|
||||
$query = $this->queryBySender($sender)->select('n');
|
||||
|
||||
if ($limit) {
|
||||
$query = $query->setMaxResults($limit);
|
||||
}
|
||||
|
||||
if ($offset) {
|
||||
$query = $query->setFirstResult($offset);
|
||||
}
|
||||
|
||||
$query->addOrderBy('n.date', 'DESC');
|
||||
|
||||
return $query->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,6 +149,31 @@ final class NotificationRepository implements ObjectRepository
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Notification[]
|
||||
*/
|
||||
public function findNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('n');
|
||||
|
||||
$qb
|
||||
->select('n')
|
||||
->where($qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'))
|
||||
->andWhere($qb->expr()->eq('n.relatedEntityId', ':relatedEntityId'))
|
||||
->andWhere($qb->expr()->isNotNull('n.sender'))
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isMemberOf(':user', 'n.addressees'),
|
||||
$qb->expr()->eq('n.sender', ':user')
|
||||
)
|
||||
)
|
||||
->setParameter('relatedEntityClass', $relatedEntityClass)
|
||||
->setParameter('relatedEntityId', $relatedEntityId)
|
||||
->setParameter('user', $user);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria, ?array $orderBy = null): ?Notification
|
||||
{
|
||||
return $this->repository->findOneBy($criteria, $orderBy);
|
||||
@ -89,22 +184,25 @@ final class NotificationRepository implements ObjectRepository
|
||||
return Notification::class;
|
||||
}
|
||||
|
||||
private function queryAllForAttendee(User $addressee, bool $countQuery = false): Query
|
||||
private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('n');
|
||||
|
||||
$select = 'n';
|
||||
|
||||
if ($countQuery) {
|
||||
$select = 'count(n)';
|
||||
}
|
||||
|
||||
$qb
|
||||
->select($select)
|
||||
->join('n.addressees', 'a')
|
||||
->where('a = :addressee')
|
||||
->where($qb->expr()->isMemberOf(':addressee', 'n.addressees'))
|
||||
->setParameter('addressee', $addressee);
|
||||
|
||||
return $qb->getQuery();
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function queryBySender(User $sender): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('n');
|
||||
|
||||
$qb
|
||||
->where($qb->expr()->eq('n.sender', ':sender'))
|
||||
->setParameter('sender', $sender);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,8 @@
|
||||
// Chill flex responsive table/block presentation
|
||||
@import './scss/flex_table';
|
||||
|
||||
// Specific templates
|
||||
@import './scss/notification';
|
||||
|
||||
/*
|
||||
* BASE LAYOUT POSITION
|
||||
|
@ -20,6 +20,7 @@ $chill-theme-buttons: (
|
||||
"misc": $gray-300,
|
||||
"cancel": $gray-300,
|
||||
"choose": $gray-300,
|
||||
"notify": $gray-300,
|
||||
"unlink": $chill-red,
|
||||
);
|
||||
|
||||
@ -73,6 +74,7 @@ $chill-theme-buttons: (
|
||||
&.btn-delete::before,
|
||||
&.btn-remove::before,
|
||||
&.btn-choose::before,
|
||||
&.btn-notify::before,
|
||||
&.btn-cancel::before {
|
||||
font: normal normal normal 14px/1 ForkAwesome;
|
||||
margin-right: 0.5em;
|
||||
@ -98,6 +100,7 @@ $chill-theme-buttons: (
|
||||
&.btn-cancel::before { content: "\f060"; } // fa-arrow-left
|
||||
&.btn-choose::before { content: "\f00c"; } // fa-check // f046 fa-check-square-o
|
||||
&.btn-unlink::before { content: "\f127"; } // fa-chain-broken
|
||||
&.btn-notify::before { content: "\f1d8"; } // fa-paper-plane
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,62 @@
|
||||
div.notification {
|
||||
h2.notification-title,
|
||||
h6.notification-title {
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
&::before {
|
||||
font-family: "ForkAwesome";
|
||||
font-size: 80%;
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
}
|
||||
div.read {
|
||||
h2.notification-title,
|
||||
h6.notification-title {
|
||||
font-weight: 500;
|
||||
&::before {
|
||||
content: "\f2b7"; //envelope-open-o
|
||||
}
|
||||
}
|
||||
}
|
||||
div.unread {
|
||||
h2.notification-title,
|
||||
h6.notification-title {
|
||||
&::before {
|
||||
content: "\f003"; //envelope-o
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Notifications List
|
||||
*/
|
||||
|
||||
div.notification-list,
|
||||
div.notification-show {
|
||||
div.item-bloc {
|
||||
div.item-row.header {
|
||||
|
||||
div.item-col {
|
||||
&:first-child {
|
||||
flex-grow: 1;
|
||||
|
||||
}
|
||||
&:last-child {
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul.small_in_title {
|
||||
list-style-type: circle;
|
||||
li {
|
||||
span.item-key {
|
||||
display: inline-block;
|
||||
width: 3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import {createApp} from "vue";
|
||||
import NotificationReadToggle from "ChillMainAssets/vuejs/_components/Notification/NotificationReadToggle.vue";
|
||||
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
|
||||
|
||||
const i18n = _createI18n({});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function (e) {
|
||||
document.querySelectorAll('.notification_toggle_read_status')
|
||||
.forEach(function (el) {
|
||||
createApp({
|
||||
template: '<notification-read-toggle ' +
|
||||
':notificationId="notificationId" ' +
|
||||
':buttonClass="buttonClass" ' +
|
||||
':buttonNoText="buttonNoText" ' +
|
||||
':showUrl="showUrl" ' +
|
||||
':isRead="isRead"' +
|
||||
'@markRead="onMarkRead" @markUnread="onMarkUnread"' +
|
||||
'></notification-read-toggle>',
|
||||
components: {
|
||||
NotificationReadToggle,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
notificationId: +el.dataset.notificationId,
|
||||
buttonClass: el.dataset.buttonClass,
|
||||
buttonNoText: 'false' === el.dataset.buttonText,
|
||||
showUrl: el.dataset.showButtonUrl,
|
||||
isRead: 1 === +el.dataset.notificationCurrentIsRead,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onMarkRead() {
|
||||
this.isRead = false;
|
||||
},
|
||||
onMarkUnread() {
|
||||
this.isRead = true;
|
||||
},
|
||||
}
|
||||
})
|
||||
.use(i18n)
|
||||
.mount(el);
|
||||
});
|
||||
})
|
@ -0,0 +1,69 @@
|
||||
import { createApp } from 'vue';
|
||||
import PickEntity from 'ChillMainAssets/vuejs/PickEntity/PickEntity.vue';
|
||||
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
|
||||
import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
|
||||
|
||||
const i18n = _createI18n(appMessages);
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function(e) {
|
||||
|
||||
let apps = document.querySelectorAll('[data-module="pick-dynamic"]');
|
||||
|
||||
apps.forEach(function(el) {
|
||||
|
||||
const
|
||||
isMultiple = parseInt(el.dataset.multiple) === 1,
|
||||
input = document.querySelector('[data-input-uniqid="'+ el.dataset.uniqid +'"]'),
|
||||
picked = isMultiple ? JSON.parse(input.value) : [JSON.parse(input.value)];
|
||||
|
||||
createApp({
|
||||
template: '<pick-entity ' +
|
||||
':multiple="multiple" ' +
|
||||
':types="types" ' +
|
||||
':picked="picked" ' +
|
||||
':uniqid="uniqid" ' +
|
||||
'@addNewEntity="addNewEntity" ' +
|
||||
'@removeEntity="removeEntity"></pick-entity>',
|
||||
components: {
|
||||
PickEntity,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
multiple: isMultiple,
|
||||
types: JSON.parse(el.dataset.types),
|
||||
picked,
|
||||
uniqid: el.dataset.uniqid,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addNewEntity(entity) {
|
||||
console.log('addNewEntity', entity);
|
||||
if (this.multiple) {
|
||||
console.log('adding multiple');
|
||||
if (!this.picked.some(el => {
|
||||
return el.type === entity.type && el.id === entity.id;
|
||||
})) {
|
||||
this.picked.push(entity);
|
||||
input.value = JSON.stringify(this.picked);
|
||||
}
|
||||
} else {
|
||||
if (!this.picked.some(el => {
|
||||
return el.type === entity.type && el.id === entity.id;
|
||||
})) {
|
||||
this.picked.splice(0, this.picked.length);
|
||||
this.picked.push(entity);
|
||||
input.value = JSON.stringify(this.picked[0]);
|
||||
}
|
||||
}
|
||||
},
|
||||
removeEntity(entity) {
|
||||
console.log('removeEntity', entity);
|
||||
this.picked = this.picked.filter(e => !(e.type === entity.type && e.id === entity.id));
|
||||
input.value = JSON.stringify(this.picked);
|
||||
},
|
||||
}
|
||||
})
|
||||
.use(i18n)
|
||||
.mount(el);
|
||||
});
|
||||
});
|
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<ul class="list-suggest remove-items">
|
||||
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type+p.id">
|
||||
<span class="chill_denomination">{{ p.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<AddPersons
|
||||
:options="addPersonsOptions"
|
||||
:key="uniqid"
|
||||
:buttonTitle="translatedListOfTypes"
|
||||
:modalTitle="translatedListOfTypes"
|
||||
ref="addPersons"
|
||||
@addNewPersons="addNewEntity"
|
||||
>
|
||||
</AddPersons>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
|
||||
import { appMessages } from "./i18n";
|
||||
|
||||
export default {
|
||||
name: "PickEntity",
|
||||
props: {
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
types: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
picked: {
|
||||
required: true,
|
||||
},
|
||||
uniqid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
emits: ['addNewEntity', 'removeEntity'],
|
||||
components: {
|
||||
AddPersons,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
key: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
addPersonsOptions() {
|
||||
return {
|
||||
uniq: !this.multiple,
|
||||
type: this.types,
|
||||
priority: null,
|
||||
button: {
|
||||
size: 'btn-sm',
|
||||
class: 'btn-submit',
|
||||
},
|
||||
};
|
||||
},
|
||||
translatedListOfTypes() {
|
||||
let trans = [];
|
||||
this.types.forEach(t => {
|
||||
trans.push(appMessages.fr.pick_entity[t].toLowerCase());
|
||||
})
|
||||
return appMessages.fr.pick_entity.modal_title + trans.join(', ');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addNewEntity({ selected, modal }) {
|
||||
selected.forEach((item) => {
|
||||
this.$emit('addNewEntity', item.result);
|
||||
}, this
|
||||
);
|
||||
this.$refs.addPersons.resetSearch(); // to cast child method
|
||||
modal.showModal = false;
|
||||
},
|
||||
removeEntity(entity) {
|
||||
console.log('remove entity', entity);
|
||||
this.$emit('removeEntity', entity);
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
@ -0,0 +1,17 @@
|
||||
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n';
|
||||
|
||||
const appMessages = {
|
||||
fr: {
|
||||
pick_entity: {
|
||||
add: 'Ajouter',
|
||||
modal_title: 'Ajouter des ',
|
||||
user: 'Utilisateurs',
|
||||
person: 'Usagers',
|
||||
thirdparty: 'Tiers',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(appMessages.fr, personMessages.fr);
|
||||
|
||||
export { appMessages };
|
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div :class="{'btn-group btn-group-sm float-end': isButtonGroup }"
|
||||
role="group" aria-label="Notification actions">
|
||||
|
||||
<button v-if="isRead"
|
||||
class="btn"
|
||||
:class="overrideClass"
|
||||
type="button"
|
||||
:title="$t('markAsUnread')"
|
||||
@click="markAsUnread"
|
||||
>
|
||||
<i class="fa fa-sm fa-envelope-o"></i>
|
||||
<span v-if="!buttonNoText" class="ps-2">
|
||||
{{ $t('markAsUnread') }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button v-if="!isRead"
|
||||
class="btn"
|
||||
:class="overrideClass"
|
||||
type="button"
|
||||
:title="$t('markAsRead')"
|
||||
@click="markAsRead"
|
||||
>
|
||||
<i class="fa fa-sm fa-envelope-open-o"></i>
|
||||
<span v-if="!buttonNoText" class="ps-2">
|
||||
{{ $t('markAsRead') }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<a v-if="isButtonGroup"
|
||||
type="button"
|
||||
class="btn btn-outline-primary"
|
||||
:href="showUrl"
|
||||
:title="$t('action.show')"
|
||||
>
|
||||
<i class="fa fa-sm fa-eye"></i>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods.js';
|
||||
|
||||
export default {
|
||||
name: "NotificationReadToggle",
|
||||
props: {
|
||||
isRead: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
},
|
||||
notificationId: {
|
||||
required: true,
|
||||
type: Number,
|
||||
},
|
||||
// Optional
|
||||
buttonClass: {
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
buttonNoText: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
},
|
||||
showUrl: {
|
||||
required: false,
|
||||
type: String
|
||||
}
|
||||
},
|
||||
emits: ['markRead', 'markUnread'],
|
||||
computed: {
|
||||
/// [Option] override default button appearance (btn-misc)
|
||||
overrideClass() {
|
||||
return this.buttonClass ? this.buttonClass : 'btn-misc'
|
||||
},
|
||||
/// [Option] don't display text on button
|
||||
buttonHideText() {
|
||||
return this.buttonNoText;
|
||||
},
|
||||
/// [Option] showUrl is href for show page second button.
|
||||
// When passed, the component return a button-group with 2 buttons.
|
||||
isButtonGroup() {
|
||||
return !!this.showUrl
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markAsUnread() {
|
||||
makeFetch('POST', `/api/1.0/main/notification/${this.notificationId}/mark/unread`, []).then(response => {
|
||||
this.$emit('markRead', { notificationId: this.notificationId });
|
||||
})
|
||||
},
|
||||
markAsRead() {
|
||||
makeFetch('POST', `/api/1.0/main/notification/${this.notificationId}/mark/read`, []).then(response => {
|
||||
this.$emit('markUnread', { notificationId: this.notificationId });
|
||||
})
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
messages: {
|
||||
fr: {
|
||||
markAsUnread: 'Marquer comme non-lu',
|
||||
markAsRead: 'Marquer comme lu'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
@ -215,3 +215,8 @@
|
||||
{{ form_widget(form.center) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block pick_user_dynamic_widget %}
|
||||
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
|
||||
<div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div>
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,88 @@
|
||||
<div class="item-bloc {% if not notification.isReadBy(app.user) %}unread{% else %}read{% endif %}">
|
||||
<div class="item-row title">
|
||||
<h2 class="notification-title">
|
||||
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}) }}">
|
||||
{{ notification.title }}
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="item-row mt-2 header">
|
||||
|
||||
<div class="item-col">
|
||||
<ul class="small_in_title">
|
||||
{% if step is not defined or step == 'inbox' %}
|
||||
<li class="notification-from">
|
||||
<span class="item-key">
|
||||
<abbr title="{{ 'notification.received_from'|trans }}">
|
||||
{{ 'notification.from'|trans }} :
|
||||
</abbr>
|
||||
</span>
|
||||
{% if not notification.isSystem %}
|
||||
<span class="badge-user">
|
||||
{{ notification.sender|chill_entity_render_string }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge-user system">{{ 'notification.is_system'|trans }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if notification.addressees|length > 0 %}
|
||||
<li class="notification-to">
|
||||
<span class="item-key">
|
||||
<abbr title="{{ 'notification.sent_to'|trans }}">
|
||||
{{ 'notification.to'|trans }} :
|
||||
</abbr>
|
||||
</span>
|
||||
{% for a in notification.addressees %}
|
||||
<span class="badge-user">
|
||||
{{ a|chill_entity_render_string }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="item-col">
|
||||
{{ notification.date|format_datetime('long', 'short') }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="mx-3 flex-grow-1">
|
||||
{% include data.template with data.template_data %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row row">
|
||||
<div>
|
||||
<blockquote class="chill-user-quote">
|
||||
{{ notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
{% if action_button is not defined or action_button != 'false' %}
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
{# Vue component #}
|
||||
<span class="notification_toggle_read_status"
|
||||
data-notification-id="{{ notification.id }}"
|
||||
data-notification-current-is-read="{{ notification.isReadBy(app.user) }}"
|
||||
></span>
|
||||
</li>
|
||||
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', notification) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_main_notification_edit', {'id': notification.id}) }}"
|
||||
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', notification) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}) }}"
|
||||
class="btn btn-show" title="{{ 'Show'|trans }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
@ -0,0 +1,46 @@
|
||||
{% extends "@ChillMain/layout.html.twig" %}
|
||||
|
||||
{% block title 'notification.Notify'|trans %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-8 notification notification-new">
|
||||
<h1 class="mb-5">{{ block('title') }}</h1>
|
||||
|
||||
{{ form_start(form, { 'attr': { 'id': 'notification' }}) }}
|
||||
|
||||
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
||||
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
||||
|
||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||
|
||||
<div class="mb-3 row">
|
||||
<label class="col-form-label col-sm-4" for="notification_message">{{ form_label(form.message) }}</label>
|
||||
<div class="col-12">
|
||||
{{ form_widget(form.message) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans|chill_return_path_label }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<button type="submit" form="notification" class="btn btn-save change-icon">
|
||||
<i class="fa fa-paper-plane fa-fw"></i> {{ 'notification.Send'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,44 @@
|
||||
{% extends "@ChillMain/layout.html.twig" %}
|
||||
|
||||
{% block title 'notification.Edit notification'|trans %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-8 notification notification-edit">
|
||||
<h1 class="mb-5">{{ block('title') }}</h1>
|
||||
|
||||
{{ form_start(form, { 'attr': { 'id': 'notification' }}) }}
|
||||
|
||||
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
||||
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
||||
|
||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||
|
||||
<div class="mb-3 row">
|
||||
<label class="col-form-label col-sm-4" for="notification_message">{{ form_label(form.message) }}</label>
|
||||
<div class="col-12">
|
||||
{{ form_widget(form.message) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans|chill_return_path_label }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<button type="submit" form="notification" class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,20 @@
|
||||
{{ dest.label }},
|
||||
|
||||
{{ notification.sender.label }} a créé une notification pour vous:
|
||||
|
||||
> {{ notification.title }}
|
||||
>
|
||||
>
|
||||
{%- for line in notification.message|split("\n") %}
|
||||
> {{ line }}
|
||||
{%- if not loop.last %}
|
||||
>
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
|
||||
Vous pouvez visualiser la notification et y répondre ici:
|
||||
|
||||
{{ absolute_url(path('chill_main_notification_show', {'_locale': 'fr', 'id': notification.id }, false)) }}
|
||||
|
||||
--
|
||||
Le logiciel Chill
|
@ -0,0 +1,19 @@
|
||||
{{ dest.label }},
|
||||
|
||||
{{ comment.createdBy.label }} a créé un commentaire sur la notification "{{ comment.notification.title }}".
|
||||
|
||||
Commentaire:
|
||||
|
||||
{% for line in comment.content|split("\n") %}
|
||||
> {{ line }}
|
||||
{%- if not loop.last %}
|
||||
>
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
|
||||
Vous pouvez visualiser la notification et y répondre ici:
|
||||
|
||||
{{ absolute_url(path('chill_main_notification_show', {'_locale': 'fr', 'id': comment.notification.id }, false)) }}
|
||||
|
||||
--
|
||||
Le logiciel Chill
|
@ -0,0 +1,45 @@
|
||||
<div class="list-group my-2 notification notification-box">
|
||||
<div class="list-group-item">
|
||||
<h4>{{ 'notification.Sent'|trans }}</h4>
|
||||
</div>
|
||||
{# TODO pagination or limit #}
|
||||
{% for notification in notifications %}
|
||||
<div class="list-group-item {% if notification.isReadBy(app.user) %}read{% else %}unread{% endif %}">
|
||||
|
||||
{% if not notification.isSystem %}
|
||||
{% if notification.sender == app.user %}
|
||||
|
||||
<h6 class="notification-title">
|
||||
<abbr title="{{ 'Le ' ~ notification.date|format_date('long') ~ '\n' ~ notification.title }}">
|
||||
{{ notification.date|format_datetime('short','short') }}
|
||||
</abbr>
|
||||
{# Vue component #}
|
||||
<span class="notification_toggle_read_status"
|
||||
data-notification-id="{{ notification.id }}"
|
||||
data-notification-current-is-read="{{ notification.isReadBy(app.user) }}"
|
||||
data-show-button-url="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}) }}"
|
||||
data-button-class="btn-outline-primary"
|
||||
data-button-text="false"
|
||||
></span>
|
||||
</h6>
|
||||
|
||||
{% if notification.addressees|length > 0 %}
|
||||
<abbr title="{{ 'notification.sent_to'|trans }}">{{ 'notification.to'|trans }}:</abbr>
|
||||
{% endif %}
|
||||
|
||||
{% for a in notification.addressees %}
|
||||
<span class="badge-user">
|
||||
{{ a|chill_entity_render_string }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
<div>{{ 'notification.you were notified by %sender%'|trans({'%sender%': notification.sender|chill_entity_render_string }) }}</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div>{{ 'notification.you were notified by system'|trans }}</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
@ -0,0 +1,57 @@
|
||||
{% extends "@ChillMain/layout.html.twig" %}
|
||||
|
||||
{% block title 'notification.My own notifications'|trans %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-10 notification notification-list">
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
<ul class="nav nav-pills justify-content-center">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if step == 'inbox' %}active{% endif %}" href="{{ path('chill_main_notification_my') }}">
|
||||
{{ 'notification.Notifications received'|trans }}
|
||||
{% if unreads['inbox'] > 0 %}
|
||||
<span class="badge rounded-pill bg-danger">
|
||||
{{ unreads['inbox'] }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if step == 'sent' %}active{% endif %}" href="{{ path('chill_main_notification_sent') }}">
|
||||
{{ 'notification.Notifications sent'|trans }}
|
||||
{% if unreads['sent'] > 0 %}
|
||||
<span class="badge rounded-pill bg-danger">
|
||||
{{ unreads['sent'] }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% if datas|length == 0 %}
|
||||
{% if step == 'inbox' %}
|
||||
<p class="chill-no-data-statement">{{ 'notification.Any notification received'|trans }}</p>
|
||||
{% else %}
|
||||
<p class="chill-no-data-statement">{{ 'notification.Any notification sent'|trans }}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="flex-table">
|
||||
{% for data in datas %}
|
||||
{% set notification = data.notification %}
|
||||
{% include 'ChillMainBundle:Notification:_list_item.html.twig' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
@ -1,42 +1,121 @@
|
||||
{% extends "@ChillMain/layout.html.twig" %}
|
||||
|
||||
{% block title 'notification.show notification from %sender%'|trans(
|
||||
{ '%sender%': notification.sender|chill_entity_render_string }
|
||||
) ~ ' ' ~ notification.title %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% import '@ChillPerson/AccompanyingCourse/Comment/macro_showItem.html.twig' as m %}
|
||||
|
||||
{% macro recordAction(comment) %}
|
||||
{% if is_granted('CHILL_MAIN_NOTIFICATION_COMMENT_EDIT', comment) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_forward_return_path('chill_main_notification_show', {
|
||||
'_fragment': 'comment-' ~ comment.id,
|
||||
'edit': comment.id,
|
||||
'id': comment.notification.id
|
||||
}) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"
|
||||
></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<div id="container content">
|
||||
<div class="grid-8 centered">
|
||||
<h1>{{ "Notifications list" | trans }}</h1>
|
||||
<!-- TODO : UNREAD & READ -->
|
||||
<div class="col-10 notification notification-show">
|
||||
|
||||
{%for data in datas %}
|
||||
{% set notification = data.notification %}
|
||||
<h1>{{ 'notification.Notification'|trans }}</h1>
|
||||
|
||||
<dl class="chill_view_data">
|
||||
<dt class="inline">{{ 'Message'|trans }}</dt>
|
||||
<dd>{{ notification.message }}</dd>
|
||||
</dl>
|
||||
<div class="flex-table">
|
||||
{% include 'ChillMainBundle:Notification:_list_item.html.twig' with {
|
||||
'data': {
|
||||
'template': handler.getTemplate(notification),
|
||||
'template_data': handler.getTemplateData(notification)
|
||||
},
|
||||
'action_button': 'false'
|
||||
} %}
|
||||
</div>
|
||||
|
||||
<dl class="chill_view_data">
|
||||
<dt class="inline">{{ 'Date'|trans }}</dt>
|
||||
<dd>{{ notification.date | date('long') }}</dd>
|
||||
</dl>
|
||||
<div class="notification-comment-list my-5">
|
||||
<h2 class="chill-blue">{{ 'notification.comments_list'|trans }}</h2>
|
||||
|
||||
{% if notification.comments|length > 0 %}
|
||||
<div class="flex-table">
|
||||
{% for comment in notification.comments %}
|
||||
|
||||
<dl class="chill_view_data">
|
||||
<dt class="inline">{{ 'Sender'|trans }}</dt>
|
||||
<dd>{{ notification.sender }}</dd>
|
||||
</dl>
|
||||
{% if editedCommentForm is null or editedCommentId != comment.id %}
|
||||
{{ m.show_comment(comment, {
|
||||
'recordAction': _self.recordAction(comment)
|
||||
}) }}
|
||||
{% else %}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row row">
|
||||
<a id="comment-{{ comment.id }}"></a>
|
||||
|
||||
<dl class="chill_view_data">
|
||||
<dt class="inline">{{ 'Addressees'|trans }}</dt>
|
||||
<dd>{{ notification.addressees |join(', ') }}</dd>
|
||||
</dl>
|
||||
{{ form_start(editedCommentForm) }}
|
||||
{{ form_errors(editedCommentForm) }}
|
||||
{{ form_widget(editedCommentForm.content) }}
|
||||
<input type="hidden" name="form" value="edit" />
|
||||
<ul class="record_actions">
|
||||
<li class="cancel">
|
||||
<a href="{{ chill_path_forward_return_path('chill_main_notification_show', {
|
||||
'_fragment': 'comment-' ~ comment.id,
|
||||
'id': notification.id }) }}" class="btn btn-cancel">
|
||||
{{ 'cancel'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
{{ form_end(editedCommentForm) }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="new-comment my-5">
|
||||
<h2 class="chill-blue">{{ 'Write a new comment'|trans }}</h2>
|
||||
|
||||
{{ form_start(appendCommentForm) }}
|
||||
{{ form_errors(appendCommentForm) }}
|
||||
{{ form_widget(appendCommentForm.content) }}
|
||||
<input type="hidden" name="form" value="append" />
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button class="btn btn-create" type="submit">{{ 'notification.append_comment'|trans }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
{{ form_end(appendCommentForm) }}
|
||||
|
||||
<dl class="chill_view_data">
|
||||
<dt class="inline">{{ 'Entity'|trans }}</dt>
|
||||
<dd>
|
||||
{% include data.template with data.template_data %}
|
||||
</dd>
|
||||
</dl>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ chill_return_path_or('chill_main_notification_my') }}" class="btn btn-cancel">
|
||||
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{# Vue component #}
|
||||
<span class="notification_toggle_read_status"
|
||||
data-notification-id="{{ notification.id }}"
|
||||
data-notification-current-is-read="1"
|
||||
></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@ -106,6 +106,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block js%}<!-- nothing added to js -->{% endblock %}
|
||||
{% block js %}<!-- nothing added to js -->{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -12,16 +12,27 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Routing\MenuBuilder;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Notification\Counter\NotificationByUserCounter;
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class UserMenuBuilder implements LocalMenuBuilderInterface
|
||||
{
|
||||
private NotificationByUserCounter $notificationByUserCounter;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(Security $security)
|
||||
{
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
public function __construct(
|
||||
NotificationByUserCounter $notificationByUserCounter,
|
||||
Security $security,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
$this->notificationByUserCounter = $notificationByUserCounter;
|
||||
$this->security = $security;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
public function buildMenu($menuId, \Knp\Menu\MenuItem $menu, array $parameters)
|
||||
@ -44,6 +55,20 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
|
||||
'order' => -9999999,
|
||||
'icon' => 'map-marker',
|
||||
]);
|
||||
|
||||
$nbNotifications = $this->notificationByUserCounter->countUnreadByUser($user);
|
||||
|
||||
$menu
|
||||
->addChild(
|
||||
$this->translator->trans('notification.My notifications with counter', ['nb' => $nbNotifications]),
|
||||
['route' => 'chill_main_notification_my']
|
||||
)
|
||||
->setExtras([
|
||||
'order' => 600,
|
||||
'icon' => 'envelope',
|
||||
'counter' => $nbNotifications,
|
||||
]);
|
||||
|
||||
$menu
|
||||
->addChild(
|
||||
'Change password',
|
||||
|
@ -0,0 +1,89 @@
|
||||
<?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\MainBundle\Security\Authorization;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\NotificationComment;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use UnexpectedValueException;
|
||||
|
||||
final class NotificationVoter extends Voter
|
||||
{
|
||||
/**
|
||||
* Allow to add a comment on a notification.
|
||||
*
|
||||
* May apply on both @see{NotificationComment::class} and @see{Notification::class}.
|
||||
*/
|
||||
public const COMMENT_ADD = 'CHILL_MAIN_NOTIFICATION_COMMENT_ADD';
|
||||
|
||||
public const COMMENT_EDIT = 'CHILL_MAIN_NOTIFICATION_COMMENT_EDIT';
|
||||
|
||||
public const NOTIFICATION_SEE = 'CHILL_MAIN_NOTIFICATION_SEE';
|
||||
|
||||
public const NOTIFICATION_TOGGLE_READ_STATUS = 'CHILL_MAIIN_NOTIFICATION_TOGGLE_READ_STATUS';
|
||||
|
||||
public const NOTIFICATION_UPDATE = 'CHILL_MAIN_NOTIFICATION_UPDATE';
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
{
|
||||
return $subject instanceof Notification || $subject instanceof NotificationComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $subject
|
||||
*/
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($subject instanceof Notification) {
|
||||
switch ($attribute) {
|
||||
case self::COMMENT_ADD:
|
||||
return false === $subject->isSystem() && (
|
||||
$subject->getAddressees()->contains($user) || $subject->getSender() === $user
|
||||
);
|
||||
|
||||
case self::NOTIFICATION_SEE:
|
||||
case self::NOTIFICATION_TOGGLE_READ_STATUS:
|
||||
return $subject->getSender() === $user || $subject->getAddressees()->contains($user);
|
||||
|
||||
case self::NOTIFICATION_UPDATE:
|
||||
return $subject->getSender() === $user && false === $subject->isSystem();
|
||||
|
||||
default:
|
||||
throw new UnexpectedValueException("this subject {$attribute} is not implemented");
|
||||
}
|
||||
} elseif ($subject instanceof NotificationComment) {
|
||||
switch ($attribute) {
|
||||
case self::COMMENT_ADD:
|
||||
return false === $subject->getNotification()->isSystem() && (
|
||||
$subject->getNotification()->getAddressees()->contains($user) || $subject->getNotification()->getSender() === $user
|
||||
);
|
||||
|
||||
case self::COMMENT_EDIT:
|
||||
return $subject->getCreatedBy() === $user && false === $subject->getNotification()->isSystem();
|
||||
|
||||
default:
|
||||
throw new UnexpectedValueException("this subject {$attribute} is not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnexpectedValueException();
|
||||
}
|
||||
}
|
@ -56,7 +56,7 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
|
||||
|
||||
/**
|
||||
* @param Address $address
|
||||
* @param null|string $format
|
||||
* @param string|null $format
|
||||
*/
|
||||
public function normalize($address, $format = null, array $context = [])
|
||||
{
|
||||
|
@ -22,7 +22,7 @@ class CollectionNormalizer implements NormalizerAwareInterface, NormalizerInterf
|
||||
|
||||
/**
|
||||
* @param Collection $collection
|
||||
* @param null|string $format
|
||||
* @param string|null $format
|
||||
*/
|
||||
public function normalize($collection, $format = null, array $context = [])
|
||||
{
|
||||
|
@ -0,0 +1,98 @@
|
||||
<?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 Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Chill\MainBundle\Test\PrepareClientTrait;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class NotificationApiControllerTest extends WebTestCase
|
||||
{
|
||||
use PrepareClientTrait;
|
||||
|
||||
private array $toDelete = [];
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
foreach ($this->toDelete as [$className, $id]) {
|
||||
$object = $em->find($className, $id);
|
||||
$em->remove($object);
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
public function generateDataMarkAsRead()
|
||||
{
|
||||
self::bootKernel();
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
$userRepository = self::$container->get(UserRepository::class);
|
||||
$userA = $userRepository->findOneBy(['username' => 'center a_social']);
|
||||
$userB = $userRepository->findOneBy(['username' => 'center b_social']);
|
||||
|
||||
$notification = new Notification();
|
||||
$notification
|
||||
->setMessage('Test generated')
|
||||
->setRelatedEntityClass(AccompanyingPeriod::class)
|
||||
->setRelatedEntityId(0)
|
||||
->setSender($userB)
|
||||
->addAddressee($userA)
|
||||
->setUpdatedAt(new DateTimeImmutable());
|
||||
$em->persist($notification);
|
||||
$em->refresh($notification);
|
||||
$em->flush();
|
||||
|
||||
$this->toDelete[] = [Notification::class, $notification->getId()];
|
||||
|
||||
yield [$notification->getId()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider generateDataMarkAsRead
|
||||
*/
|
||||
public function testMarkAsReadOrUnRead(int $notificationId)
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
$client->request('POST', "/api/1.0/main/notification/{$notificationId}/mark/read");
|
||||
|
||||
$this->assertResponseIsSuccessful('test marking as read');
|
||||
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
/** @var Notification $notification */
|
||||
$notification = $em->find(Notification::class, $notificationId);
|
||||
$user = self::$container->get(UserRepository::class)->findOneBy(['username' => 'center a_social']);
|
||||
|
||||
$this->assertTrue($notification->isReadBy($user));
|
||||
|
||||
$client->request('POST', "/api/1.0/main/notification/{$notificationId}/mark/unread");
|
||||
|
||||
$this->assertResponseIsSuccessful('test marking as unread');
|
||||
|
||||
$notification = $em->find(Notification::class, $notificationId);
|
||||
$user = $em->find(User::class, $user->getId());
|
||||
$em->refresh($notification);
|
||||
$em->refresh($user);
|
||||
|
||||
$this->assertFalse($notification->isReadBy($user));
|
||||
}
|
||||
}
|
123
src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php
Normal file
123
src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?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 Entity;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class NotificationTest extends KernelTestCase
|
||||
{
|
||||
private array $toDelete = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
foreach ($this->toDelete as [$className, $id]) {
|
||||
$object = $em->find($className, $id);
|
||||
$em->remove($object);
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
public function generateNotificationData()
|
||||
{
|
||||
self::bootKernel();
|
||||
$userRepository = self::$container->get(UserRepository::class);
|
||||
|
||||
$senderId = $userRepository
|
||||
->findOneBy(['username' => 'center b_social'])
|
||||
->getId();
|
||||
|
||||
$addressesIds = [];
|
||||
$addressesIds[] = $userRepository
|
||||
->findOneBy(['username' => 'center b_direction'])
|
||||
->getId();
|
||||
|
||||
yield [
|
||||
$senderId,
|
||||
$addressesIds,
|
||||
];
|
||||
}
|
||||
|
||||
public function testAddAddresseeStoreAnUread()
|
||||
{
|
||||
$notification = new Notification();
|
||||
$notification->addAddressee($user1 = new User());
|
||||
$notification->addAddressee($user2 = new User());
|
||||
$notification->getAddressees()->add($user3 = new User());
|
||||
$notification->getAddressees()->add($user4 = new User());
|
||||
|
||||
$this->assertCount(4, $notification->getAddressees());
|
||||
|
||||
// launch listener
|
||||
$notification->registerUnread();
|
||||
$this->assertCount(4, $notification->getUnreadBy());
|
||||
$this->assertContains($user1, $notification->getUnreadBy()->toArray());
|
||||
$this->assertContains($user2, $notification->getUnreadBy()->toArray());
|
||||
$this->assertContains($user3, $notification->getUnreadBy()->toArray());
|
||||
|
||||
$notification->markAsReadBy($user1);
|
||||
|
||||
$this->assertCount(3, $notification->getUnreadBy());
|
||||
$this->assertNotContains($user1, $notification->getUnreadBy()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider generateNotificationData
|
||||
*/
|
||||
public function testPrePersistComputeUnread(int $senderId, array $addressesIds)
|
||||
{
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
$notification = new Notification();
|
||||
$notification
|
||||
->setSender($em->find(User::class, $senderId))
|
||||
->setRelatedEntityId(0)
|
||||
->setRelatedEntityClass(AccompanyingPeriod::class)
|
||||
->setMessage('Fake message');
|
||||
|
||||
foreach ($addressesIds as $addresseeId) {
|
||||
$notification
|
||||
->getAddressees()->add($em->find(User::class, $addresseeId));
|
||||
}
|
||||
|
||||
$em->persist($notification);
|
||||
$em->flush();
|
||||
$em->refresh($notification);
|
||||
|
||||
$this->toDelete[] = [Notification::class, $notification->getId()];
|
||||
|
||||
$this->assertEquals($senderId, $notification->getSender()->getId());
|
||||
$this->assertCount(count($addressesIds), $notification->getUnreadBy());
|
||||
|
||||
$unreadIds = $notification->getUnreadBy()->map(static function (User $u) { return $u->getId(); });
|
||||
|
||||
foreach ($addressesIds as $addresseeId) {
|
||||
$this->assertContains($addresseeId, $unreadIds);
|
||||
}
|
||||
}
|
||||
}
|
@ -733,4 +733,43 @@ paths:
|
||||
class: 'Chill\PersonBundle\Entity\AccompanyingPeriod'
|
||||
roles:
|
||||
- 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE'
|
||||
/1.0/main/notification/{id}/mark/read:
|
||||
post:
|
||||
tags:
|
||||
- notification
|
||||
summary: mark a notification as read
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The notification id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
202:
|
||||
description: "accepted"
|
||||
403:
|
||||
description: "unauthorized"
|
||||
/1.0/main/notification/{id}/mark/unread:
|
||||
post:
|
||||
tags:
|
||||
- notification
|
||||
summary: mark a notification as unread
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The notification id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
202:
|
||||
description: "accepted"
|
||||
403:
|
||||
description: "unauthorized"
|
||||
|
||||
|
||||
|
@ -61,10 +61,11 @@ module.exports = function(encore, entries)
|
||||
encore.addEntry('mod_ckeditor5', __dirname + '/Resources/public/module/ckeditor5/index.js');
|
||||
encore.addEntry('mod_disablebuttons', __dirname + '/Resources/public/module/disable-buttons/index.js');
|
||||
encore.addEntry('mod_blur', __dirname + '/Resources/public/module/blur/index.js');
|
||||
|
||||
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');
|
||||
encore.addEntry('mod_notification_toggle_read_status', __dirname + '/Resources/public/module/notification/toggle_read.js');
|
||||
encore.addEntry('mod_pickentity_type', __dirname + '/Resources/public/module/pick-entity/index.js');
|
||||
|
||||
// Vue entrypoints
|
||||
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');
|
||||
encore.addEntry('vue_onthefly', __dirname + '/Resources/public/vuejs/OnTheFly/index.js');
|
||||
|
||||
};
|
||||
|
@ -18,8 +18,8 @@ services:
|
||||
|
||||
Chill\MainBundle\Form\Type\:
|
||||
resource: '../Form/Type'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
Chill\MainBundle\Doctrine\Event\:
|
||||
resource: '../Doctrine/Event/'
|
||||
|
@ -13,4 +13,58 @@ services:
|
||||
$translator: '@Symfony\Component\Translation\TranslatorInterface'
|
||||
$routeParameters: '%chill_main.notifications%'
|
||||
|
||||
Chill\MainBundle\Notification\NotificationRenderer: ~
|
||||
Chill\MainBundle\Notification\NotificationHandlerManager:
|
||||
arguments:
|
||||
$handlers: !tagged_iterator chill_main.notification_handler
|
||||
|
||||
Chill\MainBundle\Notification\NotificationPresence: ~
|
||||
|
||||
Chill\MainBundle\Notification\Templating\NotificationTwigExtension: ~
|
||||
|
||||
Chill\MainBundle\Notification\Templating\NotificationTwigExtensionRuntime: ~
|
||||
|
||||
Chill\MainBundle\Notification\Counter\NotificationByUserCounter:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
tags:
|
||||
-
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'preFlush'
|
||||
entity: 'Chill\MainBundle\Entity\Notification'
|
||||
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||
lazy: true
|
||||
method: 'onPreFlushNotification'
|
||||
|
||||
-
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'postUpdate'
|
||||
entity: 'Chill\MainBundle\Entity\NotificationComment'
|
||||
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||
lazy: true
|
||||
method: 'onEditNotificationComment'
|
||||
-
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'postPersist'
|
||||
entity: 'Chill\MainBundle\Entity\NotificationComment'
|
||||
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||
lazy: true
|
||||
method: 'onEditNotificationComment'
|
||||
|
||||
Chill\MainBundle\Notification\Email\NotificationMailer:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
-
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'postPersist'
|
||||
entity: 'Chill\MainBundle\Entity\Notification'
|
||||
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||
lazy: true
|
||||
method: 'postPersistNotification'
|
||||
-
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'postPersist'
|
||||
entity: 'Chill\MainBundle\Entity\NotificationComment'
|
||||
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||
lazy: true
|
||||
method: 'postPersistComment'
|
||||
|
@ -24,6 +24,8 @@ services:
|
||||
|
||||
Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory: ~
|
||||
|
||||
Chill\MainBundle\Security\Authorization\NotificationVoter: ~
|
||||
|
||||
Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface: '@Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory'
|
||||
|
||||
chill.main.security.authorization.helper:
|
||||
|
@ -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\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211225231532 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE chill_main_notification_addresses_unread');
|
||||
$this->addSql('ALTER TABLE chill_main_notification ADD read JSONB DEFAULT \'[]\'');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Store notification readed by user in a specific table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE chill_main_notification_addresses_unread (notification_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(notification_id, user_id))');
|
||||
$this->addSql('CREATE INDEX IDX_154A075FEF1A9D84 ON chill_main_notification_addresses_unread (notification_id)');
|
||||
$this->addSql('CREATE INDEX IDX_154A075FA76ED395 ON chill_main_notification_addresses_unread (user_id)');
|
||||
$this->addSql('ALTER TABLE chill_main_notification_addresses_unread ADD CONSTRAINT FK_154A075FEF1A9D84 FOREIGN KEY (notification_id) REFERENCES chill_main_notification (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_notification_addresses_unread ADD CONSTRAINT FK_154A075FA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_notification DROP read');
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?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\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211228183221 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('create unique index uniq_5bdc8067567988b4440f6072
|
||||
on chill_main_notification (relatedentityclass, relatedentityid)');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'remove unique index which prevent to notify twice an entity';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX uniq_5bdc8067567988b4440f6072');
|
||||
}
|
||||
}
|
@ -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\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211228215919 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP SEQUENCE chill_main_notification_comment_id_seq CASCADE');
|
||||
$this->addSql('DROP TABLE chill_main_notification_comment');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Notifications: add comment on notifications';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE SEQUENCE chill_main_notification_comment_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE chill_main_notification_comment (id INT NOT NULL, notification_id INT NOT NULL, content TEXT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updateAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_983BD2CFEF1A9D84 ON chill_main_notification_comment (notification_id)');
|
||||
$this->addSql('CREATE INDEX IDX_983BD2CF3174800F ON chill_main_notification_comment (createdBy_id)');
|
||||
$this->addSql('CREATE INDEX IDX_983BD2CF65FF1AEC ON chill_main_notification_comment (updatedBy_id)');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_notification_comment.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_notification_comment.updateAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE chill_main_notification_comment ADD CONSTRAINT FK_983BD2CFEF1A9D84 FOREIGN KEY (notification_id) REFERENCES chill_main_notification (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_notification_comment ADD CONSTRAINT FK_983BD2CF3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_notification_comment ADD CONSTRAINT FK_983BD2CF65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?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\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211229140308 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_notification DROP CONSTRAINT FK_5BDC806765FF1AEC');
|
||||
$this->addSql('DROP INDEX IDX_5BDC806765FF1AEC');
|
||||
$this->addSql('ALTER TABLE chill_main_notification DROP updatedAt');
|
||||
$this->addSql('ALTER TABLE chill_main_notification DROP updatedBy_id');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Notification: add updated tracking information';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_notification ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('UPDATE chill_main_notification SET updatedAt="date"');
|
||||
$this->addSql('ALTER TABLE chill_main_notification ADD updatedBy_id INT DEFAULT NULL');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_notification.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE chill_main_notification ADD CONSTRAINT FK_5BDC806765FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_5BDC806765FF1AEC ON chill_main_notification (updatedBy_id)');
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<?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\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20211230003532 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_notification DROP title');
|
||||
$this->addSql('ALTER TABLE chill_main_notification ALTER sender_id SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_notification ALTER updatedAt DROP NOT NULL');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add title and allow system notification (sender is null)';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_main_notification ADD title TEXT DEFAULT \'\' NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_notification ALTER sender_id DROP NOT NULL');
|
||||
$this->addSql('UPDATE chill_main_notification set updatedat="date"');
|
||||
$this->addSql('ALTER TABLE chill_main_notification ALTER updatedat SET NOT NULL');
|
||||
}
|
||||
}
|
@ -1,6 +1,15 @@
|
||||
years_old: >-
|
||||
{age, plural,
|
||||
{age, plural,
|
||||
one {# an}
|
||||
many {# ans}
|
||||
other {# ans}
|
||||
}
|
||||
|
||||
notification:
|
||||
My notifications with counter: >-
|
||||
{nb, plural,
|
||||
=0 {Mes notifications}
|
||||
one {Une notification}
|
||||
few {# notifications}
|
||||
other {# notifications}
|
||||
}
|
||||
|
@ -351,3 +351,33 @@ By: Par
|
||||
For: Pour
|
||||
Created for: Créé pour
|
||||
Created by: Créé par
|
||||
|
||||
notification:
|
||||
Notification: Notification
|
||||
My own notifications: Mes notifications
|
||||
Notify: Envoyer une notification
|
||||
Send: Envoyer
|
||||
Edit notification: Modifier une notification
|
||||
Notification created: Notification envoyée
|
||||
Notification updated: La notification a été mise à jour
|
||||
Any notification received: Aucune notification reçue
|
||||
Any notification sent: Aucune notification envoyée
|
||||
Notifications received: Notifications reçues
|
||||
Notifications sent: Notifications envoyées
|
||||
comment_appended: Commentaire ajouté
|
||||
append_comment: Ajouter un commentaire
|
||||
comment_updated: Commentaire mis à jour
|
||||
comments_list: Fil de commentaires
|
||||
show notification from %sender%: Voir la notification de %sender%
|
||||
is_unread: Non-lue
|
||||
is_system: notification automatique
|
||||
list: Notifications
|
||||
Sent: Envoyé
|
||||
to: À
|
||||
sent_to: Destinataire(s)
|
||||
from: De
|
||||
received_from: Expéditeur
|
||||
you were notified by %sender%: Vous avez été notifié par %sender%
|
||||
you were notified by system: Vous avez été notifié automatiquement
|
||||
subject: Objet
|
||||
|
||||
|
@ -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\PersonBundle\AccompanyingPeriod\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
use Symfony\Component\Workflow\Event\EnteredEvent;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class WorkflowEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private EngineInterface $engine;
|
||||
|
||||
private Security $security;
|
||||
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
public function __construct(Security $security, TranslatorInterface $translator, EngineInterface $engine, EntityManagerInterface $em)
|
||||
{
|
||||
$this->security = $security;
|
||||
$this->translator = $translator;
|
||||
$this->engine = $engine;
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
'workflow.accompanying_period_lifecycle.entered' => [
|
||||
'onStateEntered',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function onStateEntered(EnteredEvent $enteredEvent): void
|
||||
{
|
||||
if ($enteredEvent->getMarking()->has(AccompanyingPeriod::STEP_CONFIRMED)) {
|
||||
$this->onPeriodConfirmed($enteredEvent->getSubject());
|
||||
}
|
||||
}
|
||||
|
||||
private function onPeriodConfirmed(AccompanyingPeriod $period)
|
||||
{
|
||||
if ($period->getUser() instanceof User
|
||||
&& $period->getUser() !== $this->security->getUser()) {
|
||||
$notification = new Notification();
|
||||
$notification
|
||||
->setRelatedEntityId($period->getId())
|
||||
->setRelatedEntityClass(AccompanyingPeriod::class)
|
||||
->setTitle($this->translator->trans('period_notification.period_designated_subject'))
|
||||
->setMessage($this->engine->render(
|
||||
'@ChillPerson/Notification/accompanying_course_designation.md.twig',
|
||||
[
|
||||
'accompanyingCourse' => $period,
|
||||
]
|
||||
))
|
||||
->addAddressee($period->getUser());
|
||||
$this->em->persist($notification);
|
||||
}
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ class LoadHouseholdPosition extends Fixture
|
||||
{
|
||||
foreach (
|
||||
self::POSITIONS_DATA as [$name, $share, $allowHolder,
|
||||
$ordering, $ref, ]
|
||||
$ordering, $ref, ]
|
||||
) {
|
||||
$position = (new Position())
|
||||
->setLabel(['fr' => $name])
|
||||
|
@ -0,0 +1,45 @@
|
||||
<?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\PersonBundle\Notification;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
|
||||
|
||||
final class AccompanyingPeriodNotificationHandler implements NotificationHandlerInterface
|
||||
{
|
||||
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
|
||||
|
||||
public function __construct(AccompanyingPeriodRepository $accompanyingPeriodRepository)
|
||||
{
|
||||
$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
|
||||
}
|
||||
|
||||
public function getTemplate(Notification $notification, array $options = []): string
|
||||
{
|
||||
return 'ChillPersonBundle:AccompanyingPeriod:showInNotification.html.twig';
|
||||
}
|
||||
|
||||
public function getTemplateData(Notification $notification, array $options = []): array
|
||||
{
|
||||
return [
|
||||
'notification' => $notification,
|
||||
'period' => $this->accompanyingPeriodRepository->find($notification->getRelatedEntityId()),
|
||||
];
|
||||
}
|
||||
|
||||
public function supports(Notification $notification, array $options = []): bool
|
||||
{
|
||||
return $notification->getRelatedEntityClass() === AccompanyingPeriod::class;
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<?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\PersonBundle\Notification;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
|
||||
final class AccompanyingPeriodNotificationRenderer
|
||||
{
|
||||
public function getTemplate()
|
||||
{
|
||||
return 'ChillPersonBundle:AccompanyingPeriod:showInNotification.html.twig';
|
||||
}
|
||||
|
||||
public function getTemplateData(Notification $notification)
|
||||
{
|
||||
return ['notification' => $notification];
|
||||
}
|
||||
|
||||
public function supports(Notification $notification)
|
||||
{
|
||||
return $notification->getRelatedEntityClass() === AccompanyingPeriod::class;
|
||||
}
|
||||
}
|
@ -259,9 +259,9 @@ abbr.referrer { // still used ?
|
||||
div#dashboards {
|
||||
div.mbloc {
|
||||
& > div:not(.warnings) {
|
||||
border: 1px solid $chill-light-gray;
|
||||
//border: 1px solid $chill-light-gray;
|
||||
//border-radius: 0.35rem;
|
||||
background-color: $chill-llight-gray;
|
||||
border-radius: 0.35rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
& > div.warnings .alert {
|
||||
|
@ -20,6 +20,11 @@ span.badge-thirdparty {
|
||||
|
||||
span.badge-user {
|
||||
border-bottom-width: 1px;
|
||||
&.system {
|
||||
background-color: $chill-llight-gray;
|
||||
font-style: italic;
|
||||
color: $chill-gray;
|
||||
}
|
||||
}
|
||||
span.badge-person {
|
||||
border-bottom-color: $chill-green;
|
||||
|
@ -81,6 +81,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if form is not null %}
|
||||
<div class="new-comment my-5">
|
||||
<h2 class="chill-blue">{{ 'Write a new comment'|trans }}</h2>
|
||||
|
@ -13,14 +13,19 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('page_accompanying_course_index_person_locate') }}
|
||||
{{ encore_entry_script_tags('page_accompanying_course_index_masonry') }}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('page_accompanying_course_index_person_locate') }}
|
||||
{{ encore_entry_script_tags('page_accompanying_course_index_masonry') }}
|
||||
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="accompanyingcourse-resume">
|
||||
|
||||
<div id="dashboards" class="row g-3" data-masonry='{"percentPosition": true }'>
|
||||
|
||||
{% if 'DRAFT' == accompanyingCourse.step %}
|
||||
@ -186,3 +191,18 @@
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block block_post_menu %}
|
||||
<div class="post-menu pt-4">
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a class="btn btn-primary" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod', 'entityId': accompanyingCourse.id}) }}">
|
||||
<i class="fa fa-paper-plane fa-fw"></i>
|
||||
{{ 'notification.Notify'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{ chill_list_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) }}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,170 +1,60 @@
|
||||
{% if person is defined %}
|
||||
{% set contextEntity = { 'type': 'person', 'entity': person } %}{% endif %}
|
||||
{% if household is defined %}
|
||||
{% set contextEntity = { 'type': 'household', 'entity': household } %}{% endif %}
|
||||
|
||||
{% macro recordAction(period, contextEntity) %}
|
||||
{# TODO if enable_accompanying_course_with_multiple_persons is true ... #}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': period.id }) }}"
|
||||
class="btn btn-show" title="{{ 'See accompanying period'|trans }}">{# {{ 'See this period'|trans }} #}</a>
|
||||
</li>
|
||||
{% if period.step == 'DRAFT' and contextEntity.type == 'person' %}
|
||||
{% set person = contextEntity.entity %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_delete', { 'accompanying_period_id': period.id, 'person_id' : person.id }) }}"
|
||||
class="btn btn-delete" title="{{ 'Delete accompanying period'|trans }}">{# {{ 'Delete this period'|trans }} #}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{# DISABLED if new accompanying course, this is not necessary
|
||||
{% if person is defined %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_period_update', {'person_id' : person.id, 'period_id' : period.id } ) }}"
|
||||
class="btn btn-update" title="{{ 'Edit accompanying period'|trans }}"></a>
|
||||
</li>
|
||||
{% if period.isOpen == true %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_period_close', {'person_id' : person.id}) }}"
|
||||
class="btn btn-update change-icon">
|
||||
<i class="fa fa-fw fa-lock" aria-hidden="true"></i>
|
||||
{{'Close accompanying period'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if period.canBeReOpened(person) == true %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_period_re_open', {'person_id' : person.id, 'period_id' : period.id } ) }}"
|
||||
class="btn btn-create change-icon">
|
||||
<i class="fa fa-fw fa-unlock" aria-hidden="true"></i>
|
||||
{{'Re-open accompanying period'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% elseif household is defined %}
|
||||
TODO buttons specific for household ?
|
||||
{% endif %}
|
||||
#}
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex-table accompanyingcourse-list">
|
||||
{% for accompanying_period in accompanying_periods %}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<div class="wrap-header">
|
||||
<div class="wh-row">
|
||||
<div class="wh-col">
|
||||
<span class="h3">
|
||||
<i class="fa fa-fw fa-random"></i>
|
||||
<b>{{ accompanying_period.id }}</b>
|
||||
</span>
|
||||
{% if accompanying_period.emergency %}
|
||||
<span class="badge rounded-pill bg-danger">{{- 'Emergency'|trans|upper -}}</span>
|
||||
{% endif %}
|
||||
{% if accompanying_period.confidential %}
|
||||
<span class="badge rounded-pill bg-danger">{{- 'Confidential'|trans|upper -}}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="wh-col">
|
||||
{% if accompanying_period.step == 'DRAFT' %}
|
||||
<span class="badge bg-secondary">{{- 'Draft'|trans|upper -}}</span>
|
||||
{% elseif accompanying_period.step == 'CONFIRMED' %}
|
||||
<span class="badge bg-primary">{{- 'Confirmed'|trans|upper -}}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">{{- 'Closed'|trans|upper -}}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="wh-row">
|
||||
<div class="wh-col">
|
||||
{% if accompanying_period.closingDate == null %}
|
||||
{{ 'accompanying_period.dates_from_%opening_date%'|trans({ '%opening_date%': accompanying_period.openingDate|format_date('long') } ) }}
|
||||
{% else %}
|
||||
{{ 'accompanying_period.dates_from_%opening_date%_to_%closing_date%'|trans({
|
||||
'%opening_date%': accompanying_period.openingDate|format_date('long'),
|
||||
'%closing_date%': accompanying_period.closingDate|format_date('long')}
|
||||
) }}
|
||||
{% if accompanying_period.isOpen == false %}
|
||||
<dl class="chill_view_data">
|
||||
<dt>{{ 'Closing motive'|trans }} :</dt>
|
||||
<dd>{{ accompanying_period.closingMotive|chill_entity_render_box }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="wh-col">
|
||||
{% if chill_accompanying_periods.fields.user == 'visible' %}
|
||||
{% if accompanying_period.user %}
|
||||
<abbr class="referrer" title="{{ 'Referrer'|trans }}">ref:</abbr>
|
||||
{{ accompanying_period.user.username|chill_entity_render_box }}
|
||||
{% else %}
|
||||
<span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% for period in accompanying_periods %}
|
||||
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
{% if accompanying_period.requestorPerson is not null or accompanying_period.requestorThirdParty is not null %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'Requestor'|trans({'gender': null }) }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
{% if accompanying_period.requestorPerson is not null %}
|
||||
<span class="wl-item">
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'person', id: accompanying_period.requestorPerson.id },
|
||||
buttonText: accompanying_period.requestorPerson|chill_entity_render_string
|
||||
} %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if accompanying_period.requestorThirdParty is not null %}
|
||||
<span class="wl-item">
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'thirdparty', id: accompanying_period.requestorThirdParty.id },
|
||||
buttonText: accompanying_period.requestorThirdParty|chill_entity_render_string
|
||||
} %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'ChillPersonBundle:AccompanyingPeriod:_list_item.html.twig' with {
|
||||
'recordAction': _self.recordAction(period, contextEntity)
|
||||
} %}
|
||||
|
||||
{% if accompanying_period.participations.count > 0 %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'Participants'|trans }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
{% for p in accompanying_period.getCurrentParticipations %}
|
||||
<span class="wl-item">
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'person', id: p.person.id },
|
||||
buttonText: p.person|chill_entity_render_string
|
||||
} %}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if accompanying_period.socialIssues.count > 0 %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'Social issues'|trans }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
{% for si in accompanying_period.socialIssues %}
|
||||
<p class="wl-item">
|
||||
{{ si|chill_entity_render_box }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
|
||||
<ul class="record_actions">
|
||||
{# TODO if enable_accompanying_course_with_multiple_persons is true ... #}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': accompanying_period.id }) }}"
|
||||
class="btn btn-show" title="{{ 'See accompanying period'|trans }}">{# {{ 'See this period'|trans }} #}</a>
|
||||
</li>
|
||||
{% if accompanying_period.step == 'DRAFT' %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_delete', { 'accompanying_period_id': accompanying_period.id, 'person_id' : person.id }) }}"
|
||||
class="btn btn-delete" title="{{ 'Delete accompanying period'|trans }}">{# {{ 'Delete this period'|trans }} #}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<!-- if new accompanying course, this is not necessary
|
||||
{% if person is defined %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_period_update', {'person_id' : person.id, 'period_id' : accompanying_period.id } ) }}"
|
||||
class="btn btn-update" title="{{ 'Edit accompanying period'|trans }}"></a>
|
||||
</li>
|
||||
{% if accompanying_period.isOpen == true %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_period_close', {'person_id' : person.id}) }}"
|
||||
class="btn btn-update change-icon">
|
||||
<i class="fa fa-fw fa-lock" aria-hidden="true"></i>
|
||||
{{'Close accompanying period'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if accompanying_period.canBeReOpened(person) == true %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_period_re_open', {'person_id' : person.id, 'period_id' : accompanying_period.id } ) }}"
|
||||
class="btn btn-create change-icon">
|
||||
<i class="fa fa-fw fa-unlock" aria-hidden="true"></i>
|
||||
{{'Re-open accompanying period'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% elseif household is defined %}
|
||||
{# TODO buttons specific for household ? #}
|
||||
{% endif %}
|
||||
-->
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@ -0,0 +1,121 @@
|
||||
<div class="item-bloc accompanying-period-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
|
||||
<div class="item-row">
|
||||
<div class="wrap-header">
|
||||
<div class="wh-row">
|
||||
<div class="wh-col">
|
||||
<span class="h3">
|
||||
<i class="fa fa-fw fa-random"></i>
|
||||
<b>{{ period.id }}</b>
|
||||
</span>
|
||||
{% if period.emergency %}
|
||||
<span class="badge rounded-pill bg-danger">{{- 'Emergency'|trans|upper -}}</span>
|
||||
{% endif %}
|
||||
{% if period.confidential %}
|
||||
<span class="badge rounded-pill bg-danger">{{- 'Confidential'|trans|upper -}}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="wh-col">
|
||||
{% if period.step == 'DRAFT' %}
|
||||
<span class="badge bg-secondary">{{- 'Draft'|trans|upper -}}</span>
|
||||
{% elseif period.step == 'CONFIRMED' %}
|
||||
<span class="badge bg-primary">{{- 'Confirmed'|trans|upper -}}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">{{- 'Closed'|trans|upper -}}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="wh-row">
|
||||
<div class="wh-col">
|
||||
{% if period.closingDate == null %}
|
||||
{{ 'accompanying_period.dates_from_%opening_date%'|trans({ '%opening_date%': period.openingDate|format_date('long') } ) }}
|
||||
{% else %}
|
||||
{{ 'accompanying_period.dates_from_%opening_date%_to_%closing_date%'|trans({
|
||||
'%opening_date%': period.openingDate|format_date('long'),
|
||||
'%closing_date%': period.closingDate|format_date('long')}
|
||||
) }}
|
||||
{% if period.isOpen == false %}
|
||||
<dl class="chill_view_data">
|
||||
<dt>{{ 'Closing motive'|trans }} :</dt>
|
||||
<dd>{{ period.closingMotive|chill_entity_render_box }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="wh-col">
|
||||
{% if chill_accompanying_periods.fields.user == 'visible' %}
|
||||
{% if period.user %}
|
||||
<abbr class="referrer" title="{{ 'Referrer'|trans }}">{{ 'Referrer'|trans }}:</abbr>
|
||||
{{ period.user.username|chill_entity_render_box }}
|
||||
{% else %}
|
||||
<span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
{% if period.requestorPerson is not null or period.requestorThirdParty is not null %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'Requestor'|trans({'gender': null }) }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
{% if period.requestorPerson is not null %}
|
||||
<span class="wl-item">
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'person', id: period.requestorPerson.id },
|
||||
buttonText: period.requestorPerson|chill_entity_render_string
|
||||
} %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if period.requestorThirdParty is not null %}
|
||||
<span class="wl-item">
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'thirdparty', id: period.requestorThirdParty.id },
|
||||
buttonText: period.requestorThirdParty|chill_entity_render_string
|
||||
} %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if period.participations.count > 0 %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'Participants'|trans }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
{% for p in period.getCurrentParticipations %}
|
||||
<span class="wl-item">
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'person', id: p.person.id },
|
||||
buttonText: p.person|chill_entity_render_string
|
||||
} %}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if period.socialIssues.count > 0 %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>{{ 'Social issues'|trans }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
{% for si in period.socialIssues %}
|
||||
<p class="wl-item">
|
||||
{{ si|chill_entity_render_box }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if recordAction is defined %}
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
{{ recordAction }}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
@ -1,3 +1,26 @@
|
||||
<a href="{{ path('chill_person_accompanying_course_index', {'accompanying_period_id': notification.relatedEntityId }) }}">
|
||||
Go to Acc. period.
|
||||
</a>
|
||||
{% macro recordAction(period) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': period }) }}"
|
||||
class="btn btn-show" title="{{ 'See accompanying period'|trans }}"></a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
{% if period is not null %}
|
||||
<div class="flex-table">
|
||||
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', period) %}
|
||||
{% include 'ChillPersonBundle:AccompanyingPeriod:_list_item.html.twig' with {
|
||||
'recordAction': _self.recordAction(notification.relatedEntityId),
|
||||
'itemBlocClass': 'bg-chill-llight-gray'
|
||||
} %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning border-warning border-1">
|
||||
{{ 'This is the minimal period details'|trans ~ ': ' ~ period.id }}<br>
|
||||
{{ 'You are getting a notification for a period you are not allowed to see'|trans }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning border-warning border-1">
|
||||
{{ 'You are getting a notification for a period which does not exists any more'|trans }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% block title 'household.Household summary'|trans %}
|
||||
|
||||
{% block block_post_menu %}
|
||||
<div class="block-post-menu"></div>
|
||||
<div class="post-menu"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -0,0 +1,14 @@
|
||||
{{ 'period_notification.You are designated to a new period'|trans }}
|
||||
|
||||
{{ 'period_notification.See it online'|trans }}:
|
||||
|
||||
{{ absolute_url(path('chill_person_accompanying_course_index', {'accompanying_period_id': accompanyingCourse.id}, false)) }}
|
||||
|
||||
{{ 'period_notification.Persons are'|trans }}:
|
||||
|
||||
{% for p in accompanyingCourse.getCurrentParticipations %}
|
||||
* {{ p.person|chill_entity_render_string }}
|
||||
{% endfor %}
|
||||
|
||||
{{ 'period_notification.Social issues are'|trans }}: {% for s in accompanyingCourse.socialIssues %}{{ s|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}.
|
||||
|
@ -1,5 +1,5 @@
|
||||
{#
|
||||
* Copyright (C) 2014-2021, Champs Libres Cooperative SCRLFS,
|
||||
* Copyright (C) 2014-2021, 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
|
||||
@ -38,7 +38,7 @@
|
||||
}) }}
|
||||
|
||||
{% block block_post_menu %}
|
||||
<div class="block-post-menu">
|
||||
<div class="post-menu">
|
||||
{{ chill_delegated_block('person_post_vertical_menu', { 'person': person } ) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -98,7 +98,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
|
||||
|
||||
/**
|
||||
* @param AccompanyingPeriod|null $period
|
||||
* @param null|string $format
|
||||
* @param string|null $format
|
||||
*/
|
||||
public function normalize($period, $format = null, array $context = [])
|
||||
{
|
||||
|
@ -21,7 +21,7 @@ final class AccompanyingPeriodOriginNormalizer implements NormalizerInterface
|
||||
{
|
||||
/**
|
||||
* @param Origin $origin
|
||||
* @param null|string $format
|
||||
* @param string|null $format
|
||||
*/
|
||||
public function normalize($origin, $format = null, array $context = [])
|
||||
{
|
||||
|
@ -21,7 +21,7 @@ class AccompanyingPeriodParticipationNormalizer implements NormalizerAwareInterf
|
||||
|
||||
/**
|
||||
* @param AccompanyingPeriodParticipation $participation
|
||||
* @param null|string $format
|
||||
* @param string|null $format
|
||||
*/
|
||||
public function normalize($participation, $format = null, array $context = [])
|
||||
{
|
||||
|
@ -168,7 +168,7 @@ class PersonJsonNormalizer implements
|
||||
|
||||
/**
|
||||
* @param Person $person
|
||||
* @param null|string $format
|
||||
* @param string|null $format
|
||||
*/
|
||||
public function normalize($person, $format = null, array $context = [])
|
||||
{
|
||||
|
@ -31,7 +31,7 @@ class RelationshipDocGenNormalizer implements ContextAwareNormalizerInterface, N
|
||||
|
||||
/**
|
||||
* @param Relationship $relation
|
||||
* @param null|string $format
|
||||
* @param string|null $format
|
||||
*/
|
||||
public function normalize($relation, $format = null, array $context = [])
|
||||
{
|
||||
|
@ -0,0 +1,22 @@
|
||||
<?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 AccompanyingPeriod\Workflow;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class WorkflowEventSubscriberTest extends KernelTestCase
|
||||
{
|
||||
}
|
@ -20,3 +20,9 @@ services:
|
||||
|
||||
Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface: '@Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestion'
|
||||
|
||||
Chill\PersonBundle\AccompanyingPeriod\Workflow\:
|
||||
resource: './../../AccompanyingPeriod/Workflow'
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
services:
|
||||
Chill\PersonBundle\Notification\AccompanyingPeriodNotificationRenderer:
|
||||
Chill\PersonBundle\Notification\AccompanyingPeriodNotificationHandler:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
@ -487,3 +487,14 @@ docgen:
|
||||
A basic context for accompanying period: Contexte pour les parcours
|
||||
A context for accompanying period work: Contexte pour les actions d'accompagnement
|
||||
A context for accompanying period work evaluation: Contexte pour les évaluations dans les actions d'accompagnement
|
||||
|
||||
period_notification:
|
||||
period_designated_subject: Vous êtes référent d'un parcours d'accompagnement
|
||||
You are designated to a new period: Vous avez été désigné référent d'un parcours d'accompagnement.
|
||||
Persons are: Les usagers concernés sont les suivants
|
||||
Social issues are: Les problématiques sociales renseignées sont les suivantes
|
||||
See it online: Visualisez le parcours en ligne
|
||||
|
||||
You are getting a notification for a period which does not exists any more: Cette notification ne correspond pas à une période d'accompagnement valide.
|
||||
You are getting a notification for a period you are not allowed to see: La notification fait référence à une période d'accompagnement à laquelle vous n'avez pas accès.
|
||||
This is the minimal period details: Période d'accompagnement n°
|
||||
|
@ -4,10 +4,9 @@ services:
|
||||
$taskWorkflowManager: '@Chill\TaskBundle\Workflow\TaskWorkflowManager'
|
||||
tags:
|
||||
- { name: 'twig.extension' }
|
||||
|
||||
|
||||
Chill\TaskBundle\Templating\UI\CountNotificationTask:
|
||||
autoconfigure: true
|
||||
arguments:
|
||||
$singleTaskRepository: '@Chill\TaskBundle\Repository\SingleTaskRepository'
|
||||
$cachePool: '@cache.user_data'
|
||||
tags:
|
||||
- { name: chill.count_notification.user }
|
||||
|
@ -30,7 +30,7 @@ class ThirdPartyNormalizer implements NormalizerAwareInterface, NormalizerInterf
|
||||
|
||||
/**
|
||||
* @param ThirdParty $thirdParty
|
||||
* @param null|string $format
|
||||
* @param string|null $format
|
||||
*/
|
||||
public function normalize($thirdParty, $format = null, array $context = [])
|
||||
{
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit bd95d3c96a437757b7e8f35cdfd30da9aeac1a01
|
||||
Subproject commit 7a58754daf4a82f3b5419b2fb17a2ea42f7f9e7a
|
Loading…
x
Reference in New Issue
Block a user