diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5fcc5cd90..abc577452 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ and this project adheres to
## Unreleased
+[fast_actions] improve fast-actions buttons override mechanism, fix https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/413
+[homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
* [person] accompanying course: optimisation: do not fetch some resources for the banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/409)
* [person] accompanying course: close modal when edit participation (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/420)
* [person] accompanying course: treat validation error when editing on-the-fly entities (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/420)
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index d996ff3b8..20970a799 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -80,11 +80,6 @@ parameters:
count: 1
path: src/Bundle/ChillPersonBundle/Form/ChoiceLoader/PersonChoiceLoader.php
- -
- message: "#^Foreach overwrites \\$action with its value variable\\.$#"
- count: 1
- path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
-
message: "#^Foreach overwrites \\$action with its value variable\\.$#"
count: 1
diff --git a/phpstan-critical.neon b/phpstan-critical.neon
index b214654bf..632356aa6 100644
--- a/phpstan-critical.neon
+++ b/phpstan-critical.neon
@@ -30,36 +30,6 @@ parameters:
count: 2
path: src/Bundle/ChillPersonBundle/Household/MembersEditorFactory.php
- -
- message: "#^Parameter \\$action of method Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\AccompanyingPeriodWorkRepository\\:\\:buildQueryBySocialActionWithDescendants\\(\\) has invalid type Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\SocialAction\\.$#"
- count: 1
- path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
- -
- message: "#^Parameter \\$action of method Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\AccompanyingPeriodWorkRepository\\:\\:countBySocialActionWithDescendants\\(\\) has invalid type Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\SocialAction\\.$#"
- count: 1
- path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
- -
- message: "#^Undefined variable\\: \\$action$#"
- count: 1
- path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
- -
- message: "#^Undefined variable\\: \\$limit$#"
- count: 1
- path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
- -
- message: "#^Undefined variable\\: \\$offset$#"
- count: 1
- path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
- -
- message: "#^Undefined variable\\: \\$orderBy$#"
- count: 1
- path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
-
message: "#^Variable variables are not allowed\\.$#"
count: 4
diff --git a/phpstan-types.neon b/phpstan-types.neon
index 949ff774a..2cc55255a 100644
--- a/phpstan-types.neon
+++ b/phpstan-types.neon
@@ -400,11 +400,6 @@ parameters:
count: 1
path: src/Bundle/ChillPersonBundle/Form/Type/PersonPhoneType.php
- -
- message: "#^Method Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\AccompanyingPeriodWorkRepository\\:\\:buildQueryBySocialActionWithDescendants\\(\\) has invalid return type Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\QueryBuilder\\.$#"
- count: 1
- path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 3
diff --git a/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss b/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss
index 8b88f0e78..5c1c83d06 100644
--- a/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss
+++ b/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss
@@ -34,6 +34,8 @@ p.date-label {
font-size: 18pt;
}
div.dashboard,
+h4.badge-title,
+h3.badge-title,
h2.badge-title {
ul.list-content {
font-size: 70%;
diff --git a/src/Bundle/ChillMainBundle/Controller/NotificationApiController.php b/src/Bundle/ChillMainBundle/Controller/NotificationApiController.php
index 8751aa50a..c43015efd 100644
--- a/src/Bundle/ChillMainBundle/Controller/NotificationApiController.php
+++ b/src/Bundle/ChillMainBundle/Controller/NotificationApiController.php
@@ -13,13 +13,19 @@ namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
+use Chill\MainBundle\Pagination\PaginatorFactory;
+use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Security\Authorization\NotificationVoter;
+use Chill\MainBundle\Serializer\Model\Collection;
+use Chill\MainBundle\Serializer\Model\Counter;
use Doctrine\ORM\EntityManagerInterface;
use RuntimeException;
use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
+use Symfony\Component\Serializer\SerializerInterface;
use UnexpectedValueException;
/**
@@ -29,12 +35,26 @@ class NotificationApiController
{
private EntityManagerInterface $entityManager;
+ private NotificationRepository $notificationRepository;
+
+ private PaginatorFactory $paginatorFactory;
+
private Security $security;
- public function __construct(EntityManagerInterface $entityManager, Security $security)
- {
+ private SerializerInterface $serializer;
+
+ public function __construct(
+ EntityManagerInterface $entityManager,
+ NotificationRepository $notificationRepository,
+ PaginatorFactory $paginatorFactory,
+ Security $security,
+ SerializerInterface $serializer
+ ) {
$this->entityManager = $entityManager;
+ $this->notificationRepository = $notificationRepository;
+ $this->paginatorFactory = $paginatorFactory;
$this->security = $security;
+ $this->serializer = $serializer;
}
/**
@@ -53,6 +73,37 @@ class NotificationApiController
return $this->markAs('unread', $notification);
}
+ /**
+ * @Route("/my/unread")
+ */
+ public function myUnreadNotifications(Request $request): JsonResponse
+ {
+ $total = $this->notificationRepository->countUnreadByUser($this->security->getUser());
+
+ if ($request->query->getBoolean('countOnly')) {
+ return new JsonResponse(
+ $this->serializer->serialize(new Counter($total), 'json', ['groups' => ['read']]),
+ JsonResponse::HTTP_OK,
+ [],
+ true
+ );
+ }
+ $paginator = $this->paginatorFactory->create($total);
+ $notifications = $this->notificationRepository->findUnreadByUser(
+ $this->security->getUser(),
+ $paginator->getItemsPerPage(),
+ $paginator->getCurrentPageFirstItemNumber()
+ );
+ $collection = new Collection($notifications, $paginator);
+
+ return new JsonResponse(
+ $this->serializer->serialize($collection, 'json', ['groups' => ['read']]),
+ JsonResponse::HTTP_OK,
+ [],
+ true
+ );
+ }
+
private function markAs(string $target, Notification $notification): JsonResponse
{
if (!$this->security->isGranted(NotificationVoter::NOTIFICATION_TOGGLE_READ_STATUS, $notification)) {
diff --git a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
index 71966c973..9cfffb2cf 100644
--- a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
+++ b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
@@ -193,6 +193,29 @@ final class NotificationRepository implements ObjectRepository
return $this->repository->findOneBy($criteria, $orderBy);
}
+ /**
+ * @return array|Notification[]
+ */
+ public function findUnreadByUser(User $user, int $limit = 20, int $offset = 0): array
+ {
+ $rsm = new Query\ResultSetMappingBuilder($this->em);
+ $rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
+
+ $sql = 'SELECT ' . $rsm->generateSelectClause(['cmn' => 'cmn']) . ' ' .
+ 'FROM chill_main_notification cmn ' .
+ 'WHERE ' .
+ 'EXISTS (select 1 FROM chill_main_notification_addresses_unread cmnau WHERE cmnau.user_id = :userId and cmnau.notification_id = cmn.id) ' .
+ 'ORDER BY cmn.date DESC ' .
+ 'LIMIT :limit OFFSET :offset';
+
+ $nq = $this->em->createNativeQuery($sql, $rsm)
+ ->setParameter('userId', $user->getId())
+ ->setParameter('limit', $limit)
+ ->setParameter('offset', $offset);
+
+ return $nq->getResult();
+ }
+
public function getClassName()
{
return Notification::class;
diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss
index a95c4b993..381ac9271 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss
+++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss
@@ -136,3 +136,18 @@ $chill-theme-buttons: (
.btn-sm, .btn-group-sm > .btn {
min-width: 36px;
}
+
+// Homepage special fast action buttons
+div.sticky-buttons {
+ position: fixed;
+ bottom: 3em;
+ right: 2em;
+ .btn-circle {
+ width: 50px; height: 50px;
+ border-radius: 50%;
+ text-align: center;
+ padding: 0.45rem 0.7rem;
+ display: block;
+ margin-bottom: 0.5rem;
+ }
+}
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/page/homepage_widget/index.js b/src/Bundle/ChillMainBundle/Resources/public/page/homepage_widget/index.js
new file mode 100644
index 000000000..698302ebb
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/page/homepage_widget/index.js
@@ -0,0 +1,16 @@
+import { createApp } from 'vue';
+import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
+import { appMessages } from 'ChillMainAssets/vuejs/HomepageWidget/js/i18n';
+import { store } from 'ChillMainAssets/vuejs/HomepageWidget/js/store';
+import App from 'ChillMainAssets/vuejs/HomepageWidget/App';
+
+const i18n = _createI18n(appMessages);
+
+const app = createApp({
+ template: ``,
+})
+.use(store)
+.use(i18n)
+.component('app', App)
+.mount('#homepage_widget')
+;
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue
new file mode 100644
index 000000000..da3b9ba71
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue
@@ -0,0 +1,140 @@
+
+
+ {{ $t('main_title') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyAccompanyingCourses.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyAccompanyingCourses.vue
new file mode 100644
index 000000000..220588914
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyAccompanyingCourses.vue
@@ -0,0 +1,60 @@
+
+ {{ $t('my_accompanying_courses.description') }}
+ {{ $t('no_data') }}
+
+
+ id |
+ Ouvert le |
+ Usagers concernés |
+ |
+
+
+
+ {{ c.id}} |
+ {{ $d(c.openingDate.datetime, 'long') }} |
+ {{ c.participations.length }} |
+
+
+ {{ $t('show_entity', { entity: $t('the_course') }) }}
+
+ |
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue
new file mode 100644
index 000000000..65de954ee
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue
@@ -0,0 +1,77 @@
+
+ {{ $t('no_dashboard') }}
+
+
+
+
+
+ -
+ {{ counter.notifications }} {{ $t('counter.unread_notifications') }}
+
+ -
+ {{ counter.accompanyingCourses }} {{ $t('counter.assignated_courses') }}
+
+ -
+ {{ counter.works }} {{ $t('counter.assignated_actions') }}
+
+ -
+ {{ counter.evaluations }} {{ $t('counter.assignated_evaluations') }}
+
+ -
+ {{ counter.tasksAlert }} {{ $t('counter.alert_tasks') }}
+
+ -
+ {{ counter.tasksWarning }} {{ $t('counter.warning_tasks') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyEvaluations.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyEvaluations.vue
new file mode 100644
index 000000000..baaf926f9
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyEvaluations.vue
@@ -0,0 +1,57 @@
+
+ {{ $t('my_evaluations.description') }}
+ {{ $t('no_data') }}
+
+
+ id |
+ |
+
+
+
+ {{ e.id}} |
+
+
+ {{ $t('show_entity', { entity: $t('the_evaluation') }) }}
+
+ |
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyNotifications.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyNotifications.vue
new file mode 100644
index 000000000..135532f0b
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyNotifications.vue
@@ -0,0 +1,96 @@
+
+ {{ $t('my_notifications.description') }}
+ {{ $t('no_data') }}
+
+
+ {{ $t('Date') }} |
+ {{ $t('Subject') }} |
+ {{ $t('From') }} |
+ |
+
+
+
+ {{ $d(n.date.datetime, 'long') }} |
+
+
+
+ {{ n.title }}
+
+ |
+ {{ n.sender.text }} |
+
+
+ {{ $t('show_entity', { entity: getEntityName(n) }) }}
+
+ |
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyTasks.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyTasks.vue
new file mode 100644
index 000000000..c8bdcb256
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyTasks.vue
@@ -0,0 +1,85 @@
+
+
+ {{ $t('my_tasks.description_alert') }}
+ {{ $t('no_data') }}
+
+
+ id |
+ |
+
+
+
+ {{ t.id}} |
+
+
+ {{ $t('show_entity', { entity: $t('the_task') }) }}
+
+ |
+
+
+
+
+ {{ $t('my_tasks.description_warning') }}
+ {{ $t('no_data') }}
+
+
+ id |
+ |
+
+
+
+ {{ t.id}} |
+
+
+ {{ $t('show_entity', { entity: $t('the_task') }) }}
+
+ |
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyWorks.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyWorks.vue
new file mode 100644
index 000000000..19e3a92be
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyWorks.vue
@@ -0,0 +1,79 @@
+
+
+
{{ $t('my_works.description') }}
+
{{ $t('no_data') }}
+
+
+ {{ $t('StartDate') }} |
+ {{ $t('SocialAction') }} |
+ |
+
+
+
+ {{ $d(w.startDate.datetime, 'short') }} |
+
+
+
+
+ {{ w.socialAction.text }}
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/TabCounter.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/TabCounter.vue
new file mode 100644
index 000000000..5587e7176
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/TabCounter.vue
@@ -0,0 +1,18 @@
+
+
+ {{ count }}
+
+
+
+
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/TabTable.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/TabTable.vue
new file mode 100644
index 000000000..15a2fe632
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/TabTable.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js
new file mode 100644
index 000000000..724ca3d34
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js
@@ -0,0 +1,54 @@
+const appMessages = {
+ fr: {
+ main_title: "Vue d'ensemble",
+ my_works: {
+ tab: "Mes actions",
+ description: "Liste des actions d'accompagnement dont je suis référent et qui arrivent à échéance.",
+ },
+ my_evaluations: {
+ tab: "Mes évaluations",
+ description: "Liste des évaluations dont je suis référent et qui arrivent à échéance.",
+ },
+ my_tasks: {
+ tab: "Mes tâches",
+ description_alert: "Liste des tâches auxquelles je suis assigné et dont la date de rappel est dépassée.",
+ description_warning: "Liste des tâches auxquelles je suis assigné et dont la date d'échéance est dépassée.",
+ },
+ my_accompanying_courses: {
+ tab: "Mes parcours",
+ description: "Liste des parcours d'accompagnement que l'on vient de m'attribuer.",
+ },
+ my_notifications: {
+ tab: "Mes notifications",
+ description: "Liste des notifications reçues et non lues.",
+ },
+ Date: "Date",
+ From: "De",
+ Subject: "Objet",
+ Entity: "Associé à",
+ show_entity: "Voir {entity}",
+ the_activity: "l'échange",
+ the_course: "le parcours",
+ the_action: "l'action",
+ the_evaluation: "l'évaluation",
+ the_task: "la tâche",
+ StartDate: "Date d'ouverture",
+ SocialAction: "Action d'accompagnement",
+ no_data: "Aucun résultats",
+ no_dashboard: "Pas de tableaux de bord",
+ counter: {
+ unread_notifications: "notifications non lues",
+ assignated_courses: "parcours récents assignés",
+ assignated_actions: "actions assignées",
+ assignated_evaluations: "évaluations assignées",
+ alert_tasks: "tâches en rappel",
+ warning_tasks: "tâches à échéances",
+ }
+ }
+};
+
+Object.assign(appMessages.fr);
+
+export {
+ appMessages
+};
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js
new file mode 100644
index 000000000..81896e5ec
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js
@@ -0,0 +1,200 @@
+import 'es6-promise/auto';
+import { createStore } from 'vuex';
+import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
+import MyCustoms from "../MyCustoms";
+import MyWorks from "../MyWorks";
+import MyEvaluations from "../MyEvaluations";
+import MyTasks from "../MyTasks";
+import MyAccompanyingCourses from "../MyAccompanyingCourses";
+import MyNotifications from "../MyNotifications";
+
+const debug = process.env.NODE_ENV !== 'production';
+
+const isEmpty = (obj) => {
+ return obj
+ && Object.keys(obj).length <= 1
+ && Object.getPrototypeOf(obj) === Object.prototype;
+};
+
+const store = createStore({
+ strict: debug,
+ state: {
+ works: {},
+ evaluations: {},
+ tasks: {
+ warning: {},
+ alert: {}
+ },
+ accompanyingCourses: {},
+ notifications: {},
+ errorMsg: [],
+ loading: false
+ },
+ getters: {
+ isWorksLoaded(state) {
+ return !isEmpty(state.works);
+ },
+ isEvaluationsLoaded(state) {
+ return !isEmpty(state.evaluations);
+ },
+ isTasksWarningLoaded(state) {
+ return !isEmpty(state.tasks.warning);
+ },
+ isTasksAlertLoaded(state) {
+ return !isEmpty(state.tasks.alert);
+ },
+ isAccompanyingCoursesLoaded(state) {
+ return !isEmpty(state.accompanyingCourses);
+ },
+ isNotificationsLoaded(state) {
+ return !isEmpty(state.notifications);
+ },
+ counter(state) {
+ return {
+ works: state.works.count,
+ evaluations: state.evaluations.count,
+ tasksWarning: state.tasks.warning.count,
+ tasksAlert: state.tasks.alert.count,
+ accompanyingCourses: state.accompanyingCourses.count,
+ notifications: state.notifications.count,
+ }
+ }
+ },
+ mutations: {
+ addWorks(state, works) {
+ console.log('addWorks', works);
+ state.works = works;
+ },
+ addEvaluations(state, evaluations) {
+ console.log('addEvaluations', evaluations);
+ state.evaluations = evaluations;
+ },
+ addTasksWarning(state, tasks) {
+ console.log('addTasksWarning', tasks);
+ state.tasks.warning = tasks;
+ },
+ addTasksAlert(state, tasks) {
+ console.log('addTasksAlert', tasks);
+ state.tasks.alert = tasks;
+ },
+ addCourses(state, courses) {
+ console.log('addCourses', courses);
+ state.accompanyingCourses = courses;
+ },
+ addNotifications(state, notifications) {
+ console.log('addNotifications', notifications);
+ state.notifications = notifications;
+ },
+ setLoading(state, bool) {
+ state.loading = bool;
+ },
+ catchError(state, error) {
+ state.errorMsg.push(error);
+ }
+ },
+ actions: {
+ getByTab({ commit, getters }, { tab, param }) {
+ switch (tab) {
+ case 'MyCustoms':
+ break;
+ case 'MyWorks':
+ if (!getters.isWorksLoaded) {
+ commit('setLoading', true);
+ const url = `/api/1.0/person/accompanying-period/work/my-near-end${'?'+ param}`;
+ makeFetch('GET', url)
+ .then((response) => {
+ commit('addWorks', response);
+ commit('setLoading', false);
+ })
+ .catch((error) => {
+ commit('catchError', error);
+ throw error;
+ })
+ ;
+ }
+ break;
+ case 'MyEvaluations':
+ if (!getters.isEvaluationsLoaded) {
+ commit('setLoading', true);
+ const url = `/api/1.0/person/accompanying-period/work/evaluation/my-near-end${'?'+ param}`;
+ makeFetch('GET', url)
+ .then((response) => {
+ commit('addEvaluations', response);
+ commit('setLoading', false);
+ })
+ .catch((error) => {
+ commit('catchError', error);
+ throw error;
+ })
+ ;
+ }
+ break;
+ case 'MyTasks':
+ if (!(getters.isTasksWarningLoaded && getters.isTasksAlertLoaded)) {
+ commit('setLoading', true);
+ const
+ urlWarning = `/api/1.0/task/single-task/list/my?f[q]=&f[checkboxes][status][]=warning&f[checkboxes][states][]=new&f[checkboxes][states][]=in_progress${'&'+ param}`,
+ urlAlert = `/api/1.0/task/single-task/list/my?f[q]=&f[checkboxes][status][]=alert&f[checkboxes][states][]=new&f[checkboxes][states][]=in_progress${'&'+ param}`
+ ;
+ makeFetch('GET', urlWarning)
+ .then((response) => {
+ commit('addTasksWarning', response);
+ commit('setLoading', false);
+ })
+ .catch((error) => {
+ commit('catchError', error);
+ throw error;
+ })
+ ;
+ makeFetch('GET', urlAlert)
+ .then((response) => {
+ commit('addTasksAlert', response);
+ commit('setLoading', false);
+ })
+ .catch((error) => {
+ commit('catchError', error);
+ throw error;
+ })
+ ;
+ }
+ break;
+ case 'MyAccompanyingCourses':
+ if (!getters.isAccompanyingCoursesLoaded) {
+ commit('setLoading', true);
+ const url = `/api/1.0/person/accompanying-course/list/by-recent-attributions${'?'+ param}`;
+ makeFetch('GET', url)
+ .then((response) => {
+ commit('addCourses', response);
+ commit('setLoading', false);
+ })
+ .catch((error) => {
+ commit('catchError', error);
+ throw error;
+ })
+ ;
+ }
+ break;
+ case 'MyNotifications':
+ if (!getters.isNotificationsLoaded) {
+ commit('setLoading', true);
+ const url = `/api/1.0/main/notification/my/unread${'?'+ param}`;
+ makeFetch('GET', url)
+ .then((response) => {
+ commit('addNotifications', response);
+ commit('setLoading', false);
+ })
+ .catch((error) => {
+ commit('catchError', error);
+ throw error;
+ })
+ ;
+ }
+ break;
+ default:
+ throw 'tab '+ tab;
+ }
+ }
+ },
+});
+
+export { store };
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Homepage/fast_actions.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Homepage/fast_actions.html.twig
new file mode 100644
index 000000000..9138212e2
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/views/Homepage/fast_actions.html.twig
@@ -0,0 +1,3 @@
+
+ {# Override this file to add fast actions buttons #}
+
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig
new file mode 100644
index 000000000..aec38f3c3
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig
@@ -0,0 +1,15 @@
+
+
+ {# vue component #}
+
+
+ {% include '@ChillMain/Homepage/fast_actions.html.twig' %}
+
+
+{% block css %}
+ {{ encore_entry_link_tags('page_homepage_widget') }}
+{% endblock %}
+
+{% block js %}
+ {{ encore_entry_script_tags('page_homepage_widget') }}
+{% endblock %}
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig
index ef48414e0..c11422a1b 100644
--- a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig
+++ b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig
@@ -60,26 +60,26 @@
{% endif %}
{% block content %}
-
-
{{ 'Search'|trans }}
-
-
-
-
-
- {{ chill_widget('homepage', {} ) }}
+
+
{{ 'Search'|trans }}
+
+
+
+
+ {# DISABLED {{ chill_widget('homepage', {} ) }} #}
+
+ {% include '@ChillMain/Homepage/index.html.twig' %}
+
{% endblock %}
diff --git a/src/Bundle/ChillMainBundle/Serializer/Model/Counter.php b/src/Bundle/ChillMainBundle/Serializer/Model/Counter.php
new file mode 100644
index 000000000..da1d348c7
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Serializer/Model/Counter.php
@@ -0,0 +1,41 @@
+counter = $counter;
+ }
+
+ public function getCounter(): ?int
+ {
+ return $this->counter;
+ }
+
+ public function jsonSerialize()
+ {
+ return ['count' => $this->counter];
+ }
+
+ public function setCounter(?int $counter): Counter
+ {
+ $this->counter = $counter;
+
+ return $this;
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/NotificationNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/NotificationNormalizer.php
new file mode 100644
index 000000000..08deac745
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/NotificationNormalizer.php
@@ -0,0 +1,71 @@
+notificationHandlerManager = $notificationHandlerManager;
+ $this->entityManager = $entityManager;
+ $this->security = $security;
+ }
+
+ /**
+ * @param Notification $object
+ *
+ * @return array|ArrayObject|bool|float|int|string|void|null
+ */
+ public function normalize($object, ?string $format = null, array $context = [])
+ {
+ dump($object);
+ $entity = $this->entityManager
+ ->getRepository($object->getRelatedEntityClass())
+ ->find($object->getRelatedEntityId());
+
+ return [
+ 'type' => 'notification',
+ 'id' => $object->getId(),
+ 'addressees' => $this->normalizer->normalize($object->getAddressees(), $format, $context),
+ 'date' => $this->normalizer->normalize($object->getDate(), $format, $context),
+ 'isRead' => $object->isReadBy($this->security->getUser()),
+ 'message' => $object->getMessage(),
+ 'relatedEntityClass' => $object->getRelatedEntityClass(),
+ 'relatedEntityId' => $object->getRelatedEntityId(),
+ 'sender' => $this->normalizer->normalize($object->getSender(), $format, $context),
+ 'title' => $object->getTitle(),
+ 'entity' => null !== $entity ? $this->normalizer->normalize($entity, $format, $context) : null,
+ ];
+ }
+
+ public function supportsNormalization($data, ?string $format = null)
+ {
+ return $data instanceof Notification && 'json' === $format;
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/chill.webpack.config.js b/src/Bundle/ChillMainBundle/chill.webpack.config.js
index 113d326c1..93f71dd94 100644
--- a/src/Bundle/ChillMainBundle/chill.webpack.config.js
+++ b/src/Bundle/ChillMainBundle/chill.webpack.config.js
@@ -53,6 +53,7 @@ module.exports = function(encore, entries)
encore.addEntry('page_login', __dirname + '/Resources/public/page/login/index.js');
encore.addEntry('page_location', __dirname + '/Resources/public/page/location/index.js');
encore.addEntry('page_workflow_show', __dirname + '/Resources/public/page/workflow-show/index.js');
+ encore.addEntry('page_homepage_widget', __dirname + '/Resources/public/page/homepage_widget/index.js');
buildCKEditor(encore);
diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php
index 3b051a59d..c66488869 100644
--- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php
+++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php
@@ -13,7 +13,9 @@ namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Scope;
+use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Serializer\Model\Collection;
+use Chill\MainBundle\Serializer\Model\Counter;
use Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
@@ -23,8 +25,11 @@ use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepository;
+use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
+use DateInterval;
+use DateTimeImmutable;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
@@ -32,13 +37,14 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;
+
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Workflow\Registry;
-
use function array_values;
use function count;
@@ -46,6 +52,8 @@ final class AccompanyingCourseApiController extends ApiController
{
private AccompanyingPeriodACLAwareRepository $accompanyingPeriodACLAwareRepository;
+ private AccompanyingPeriodRepository $accompanyingPeriodRepository;
+
private EventDispatcherInterface $eventDispatcher;
private ReferralsSuggestionInterface $referralAvailable;
@@ -55,17 +63,19 @@ final class AccompanyingCourseApiController extends ApiController
private ValidatorInterface $validator;
public function __construct(
- EventDispatcherInterface $eventDispatcher,
- ValidatorInterface $validator,
- Registry $registry,
+ AccompanyingPeriodRepository $accompanyingPeriodRepository,
AccompanyingPeriodACLAwareRepository $accompanyingPeriodACLAwareRepository,
- ReferralsSuggestionInterface $referralAvailable
+ EventDispatcherInterface $eventDispatcher,
+ ReferralsSuggestionInterface $referralAvailable,
+ Registry $registry,
+ ValidatorInterface $validator
) {
- $this->eventDispatcher = $eventDispatcher;
- $this->validator = $validator;
- $this->registry = $registry;
+ $this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
$this->accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository;
+ $this->eventDispatcher = $eventDispatcher;
$this->referralAvailable = $referralAvailable;
+ $this->registry = $registry;
+ $this->validator = $validator;
}
public function commentApi($id, Request $request, string $_format): Response
@@ -99,6 +109,57 @@ final class AccompanyingCourseApiController extends ApiController
]);
}
+ /**
+ * @Route("/api/1.0/person/accompanying-course/list/by-recent-attributions")
+ */
+ public function findMyRecentCourseAttribution(Request $request): JsonResponse
+ {
+ $this->denyAccessUnlessGranted('ROLE_USER');
+ $user = $this->getUser();
+
+ if (!$user instanceof User) {
+ throw new AccessDeniedException();
+ }
+
+ $since = (new DateTimeImmutable('now'))->sub(new DateInterval('P15D'));
+
+ $total = $this->accompanyingPeriodRepository->countByRecentUserHistory($user, $since);
+
+ if ($request->query->getBoolean('countOnly', false)) {
+ return new JsonResponse(
+ $this->getSerializer()->serialize(new Counter($total), 'json'),
+ JsonResponse::HTTP_OK,
+ [],
+ true
+ );
+ }
+
+ $paginator = $this->getPaginatorFactory()->create($total);
+
+ if (0 === $total) {
+ return new JsonResponse(
+ $this->getSerializer()->serialize(new Collection([], $paginator), 'json'),
+ JsonResponse::HTTP_OK,
+ [],
+ true
+ );
+ }
+
+ $courses = $this->accompanyingPeriodRepository->findByRecentUserHistory(
+ $user,
+ $since,
+ $paginator->getItemsPerPage(),
+ $paginator->getCurrentPageFirstItemNumber()
+ );
+
+ return new JsonResponse(
+ $this->getSerializer()->serialize(new Collection($courses, $paginator), 'json', ['groups' => ['read']]),
+ JsonResponse::HTTP_OK,
+ [],
+ true
+ );
+ }
+
/**
* @ParamConverter("person", options={"id": "person_id"})
*/
diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkApiController.php
index 95cbaf023..fb25a3ff7 100644
--- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkApiController.php
+++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkApiController.php
@@ -12,10 +12,54 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
+use Chill\MainBundle\Serializer\Model\Collection;
+use Chill\MainBundle\Serializer\Model\Counter;
+use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
+use DateInterval;
+use DateTimeImmutable;
+use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Annotation\Route;
class AccompanyingCourseWorkApiController extends ApiController
{
+ private AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository;
+
+ public function __construct(AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository)
+ {
+ $this->accompanyingPeriodWorkRepository = $accompanyingPeriodWorkRepository;
+ }
+
+ /**
+ * @Route("/api/1.0/person/accompanying-period/work/my-near-end")
+ */
+ public function myWorksNearEndDate(Request $request): JsonResponse
+ {
+ $since = (new DateTimeImmutable('now'))
+ ->sub(new DateInterval('P' . $request->query->getInt('since', 15) . 'D'));
+ $until = (new DateTimeImmutable('now'))
+ ->add(new DateInterval('P' . $request->query->getInt('since', 15) . 'D'));
+ $total = $this->accompanyingPeriodWorkRepository
+ ->countNearEndDateByUser($this->getUser(), $since, $until);
+
+ if ($request->query->getBoolean('countOnly', false)) {
+ return $this->json(
+ new Counter($total),
+ JsonResponse::HTTP_OK,
+ [],
+ ['groups' => ['read']]
+ );
+ }
+
+ $paginator = $this->getPaginatorFactory()->create($total);
+ $works = $this->accompanyingPeriodWorkRepository
+ ->findNearEndDateByUser($this->getUser(), $since, $until, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
+
+ $collection = new Collection($works, $paginator);
+
+ return $this->json($collection, 200, [], ['groups' => ['read']]);
+ }
+
protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array
{
switch ($action) {
diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodWorkEvaluationApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodWorkEvaluationApiController.php
index cc918c1db..1bb4bc1fe 100644
--- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodWorkEvaluationApiController.php
+++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodWorkEvaluationApiController.php
@@ -15,11 +15,15 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Serializer\Model\Collection;
+use Chill\MainBundle\Serializer\Model\Counter;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
+use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
@@ -28,20 +32,28 @@ use function in_array;
class AccompanyingPeriodWorkEvaluationApiController
{
+ private AccompanyingPeriodWorkEvaluationRepository $accompanyingPeriodWorkEvaluationRepository;
+
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
private PaginatorFactory $paginatorFactory;
+ private Security $security;
+
private SerializerInterface $serializer;
public function __construct(
+ AccompanyingPeriodWorkEvaluationRepository $accompanyingPeriodWorkEvaluationRepository,
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
SerializerInterface $serializer,
- PaginatorFactory $paginatorFactory
+ PaginatorFactory $paginatorFactory,
+ Security $security
) {
+ $this->accompanyingPeriodWorkEvaluationRepository = $accompanyingPeriodWorkEvaluationRepository;
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->serializer = $serializer;
$this->paginatorFactory = $paginatorFactory;
+ $this->security = $security;
}
/**
@@ -76,4 +88,39 @@ class AccompanyingPeriodWorkEvaluationApiController
]
), JsonResponse::HTTP_OK, [], true);
}
+
+ /**
+ * @Route("/api/1.0/person/accompanying-period/work/evaluation/my-near-end")
+ */
+ public function myWorksNearEndDate(Request $request): JsonResponse
+ {
+ $total = $this->accompanyingPeriodWorkEvaluationRepository
+ ->countNearMaxDateByUser($this->security->getUser());
+
+ if ($request->query->getBoolean('countOnly', false)) {
+ return new JsonResponse(
+ $this->serializer->serialize(new Counter($total), 'json', ['groups' => 'read']),
+ JsonResponse::HTTP_OK,
+ [],
+ true
+ );
+ }
+
+ $paginator = $this->paginatorFactory->create($total);
+ $works = $this->accompanyingPeriodWorkEvaluationRepository
+ ->findNearMaxDateByUser(
+ $this->security->getUser(),
+ $paginator->getItemsPerPage(),
+ $paginator->getCurrentPageFirstItemNumber()
+ );
+
+ $collection = new Collection($works, $paginator);
+
+ return new JsonResponse(
+ $this->serializer->serialize($collection, 'json', ['groups' => 'read']),
+ JsonResponse::HTTP_OK,
+ [],
+ true
+ );
+ }
}
diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php
index c457d2863..d71ac2b9f 100644
--- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php
+++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php
@@ -26,6 +26,7 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
+use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\AccompanyingPeriodValidity;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ParticipationOverlap;
@@ -338,6 +339,14 @@ class AccompanyingPeriod implements
*/
private ?User $user = null;
+ /**
+ * @ORM\OneToMany(targetEntity=UserHistory::class, mappedBy="accompanyingPeriod", orphanRemoval=true,
+ * cascade={"persist", "remove"})
+ *
+ * @var Collection|UserHistory[]
+ */
+ private Collection $userHistories;
+
/**
* Temporary field, which is filled when the user is changed.
*
@@ -370,6 +379,7 @@ class AccompanyingPeriod implements
$this->comments = new ArrayCollection();
$this->works = new ArrayCollection();
$this->resources = new ArrayCollection();
+ $this->userHistories = new ArrayCollection();
}
/**
@@ -1214,10 +1224,20 @@ class AccompanyingPeriod implements
return $this;
}
- public function setUser(User $user): self
+ public function setUser(?User $user): self
{
if ($this->user !== $user) {
$this->userPrevious = $this->user;
+
+ foreach ($this->userHistories as $history) {
+ if (null === $history->getEndDate()) {
+ $history->setEndDate(new DateTimeImmutable('now'));
+ }
+ }
+
+ if (null !== $user) {
+ $this->userHistories->add(new UserHistory($this, $user));
+ }
}
$this->user = $user;
diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/UserHistory.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/UserHistory.php
new file mode 100644
index 000000000..4e0e2493f
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/UserHistory.php
@@ -0,0 +1,96 @@
+startDate = $startDate ?? new DateTimeImmutable('now');
+ $this->accompanyingPeriod = $accompanyingPeriod;
+ $this->user = $user;
+ }
+
+ public function getAccompanyingPeriod(): AccompanyingPeriod
+ {
+ return $this->accompanyingPeriod;
+ }
+
+ public function getEndDate(): ?DateTimeImmutable
+ {
+ return $this->endDate;
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getStartDate(): DateTimeImmutable
+ {
+ return $this->startDate;
+ }
+
+ public function getUser(): User
+ {
+ return $this->user;
+ }
+
+ public function setEndDate(?DateTimeImmutable $endDate): UserHistory
+ {
+ $this->endDate = $endDate;
+
+ return $this;
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationRepository.php
index 06634b3cc..3178aa6e2 100644
--- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationRepository.php
+++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationRepository.php
@@ -11,9 +11,12 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
+use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
+use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
+use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
@@ -25,6 +28,12 @@ class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
$this->repository = $entityManager->getRepository(AccompanyingPeriodWorkEvaluation::class);
}
+ public function countNearMaxDateByUser(User $user): int
+ {
+ return $this->buildQueryNearMaxDateByUser($user)
+ ->select('count(e)')->getQuery()->getSingleScalarResult();
+ }
+
public function find($id): ?AccompanyingPeriodWorkEvaluation
{
return $this->repository->find($id);
@@ -39,8 +48,8 @@ class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
}
/**
- * @param null|mixed $limit
- * @param null|mixed $offset
+ * @param int $limit
+ * @param int $offset
*
* @return array|AccompanyingPeriodWorkEvaluation[]
*/
@@ -49,13 +58,45 @@ class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
- public function findOneBy(array $criteria): ?AccompanyingPeriodWorkEvaluation
+ public function findNearMaxDateByUser(User $user, int $limit = 20, int $offset = 0): array
{
- return $this->findOneBy($criteria);
+ return $this->buildQueryNearMaxDateByUser($user)
+ ->select('e')
+ ->setFirstResult($offset)
+ ->setMaxResults($limit)
+ ->getQuery()
+ ->getResult();
}
- public function getClassName()
+ public function findOneBy(array $criteria): ?AccompanyingPeriodWorkEvaluation
+ {
+ return $this->repository->findOneBy($criteria);
+ }
+
+ public function getClassName(): string
{
return AccompanyingPeriodWorkEvaluation::class;
}
+
+ private function buildQueryNearMaxDateByUser(User $user): QueryBuilder
+ {
+ $qb = $this->repository->createQueryBuilder('e');
+
+ $qb
+ ->join('e.accompanyingPeriodWork', 'work')
+ ->join('work.accompanyingPeriod', 'period')
+ ->where(
+ $qb->expr()->andX(
+ $qb->expr()->eq('period.user', ':user'),
+ $qb->expr()->isNull('e.endDate'),
+ $qb->expr()->gte(':now', $qb->expr()->diff('e.maxDate', 'e.warningInterval'))
+ )
+ )
+ ->setParameters([
+ 'user' => $user,
+ 'now' => new DateTimeImmutable('now'),
+ ]);
+
+ return $qb;
+ }
}
diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
index 23ccf57a9..70e972aa5 100644
--- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
+++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
@@ -11,10 +11,14 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
+use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
+use Chill\PersonBundle\Entity\SocialWork\SocialAction;
+use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
+use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
final class AccompanyingPeriodWorkRepository implements ObjectRepository
@@ -41,6 +45,12 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
->getSingleScalarResult();
}
+ public function countNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until): int
+ {
+ return $this->buildQueryNearEndDateByUser($user, $since, $until)
+ ->select('count(w)')->getQuery()->getSingleScalarResult();
+ }
+
public function find($id): ?AccompanyingPeriodWork
{
return $this->repository->find($id);
@@ -68,6 +78,16 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
return $this->repository->findByAccompanyingPeriod($period, $orderBy, $limit, $offset);
}
+ public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array
+ {
+ return $this->buildQueryNearEndDateByUser($user, $since, $until)
+ ->select('w')
+ ->setFirstResult($offset)
+ ->setMaxResults($limit)
+ ->getQuery()
+ ->getResult();
+ }
+
public function findOneBy(array $criteria): ?AccompanyingPeriodWork
{
return $this->repository->findOneBy($criteria);
@@ -78,22 +98,6 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
return AccompanyingPeriodWork::class;
}
- public function toDelete()
- {
- $qb = $this->buildQueryBySocialActionWithDescendants($action);
- $qb->select('g');
-
- foreach ($orderBy as $sort => $order) {
- $qb->addOrderBy('g.' . $sort, $order);
- }
-
- return $qb
- ->setMaxResults($limit)
- ->setFirstResult($offset)
- ->getQuery()
- ->getResult();
- }
-
private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder
{
$actions = $action->getDescendantsWithThis();
@@ -103,12 +107,34 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
$orx = $qb->expr()->orX();
$i = 0;
- foreach ($actions as $action) {
+ foreach ($actions as $a) {
$orx->add(":action_{$i} MEMBER OF g.socialActions");
- $qb->setParameter("action_{$i}", $action);
+ $qb->setParameter("action_{$i}", $a);
}
$qb->where($orx);
return $qb;
}
+
+ private function buildQueryNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until): QueryBuilder
+ {
+ $qb = $this->repository->createQueryBuilder('w');
+
+ $qb
+ ->join('w.accompanyingPeriod', 'period')
+ ->where(
+ $qb->expr()->andX(
+ $qb->expr()->eq('period.user', ':user'),
+ $qb->expr()->gte('w.endDate', ':since'),
+ $qb->expr()->lte('w.startDate', ':until')
+ )
+ )
+ ->setParameters([
+ 'user' => $user,
+ 'since' => $since,
+ 'until' => $until,
+ ]);
+
+ return $qb;
+ }
}
diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodRepository.php
index edb4b4f3d..3f0c30e47 100644
--- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodRepository.php
+++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodRepository.php
@@ -11,7 +11,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository;
+use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
+use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
@@ -26,6 +28,13 @@ final class AccompanyingPeriodRepository implements ObjectRepository
$this->repository = $entityManager->getRepository(AccompanyingPeriod::class);
}
+ public function countByRecentUserHistory(User $user, DateTimeImmutable $since): int
+ {
+ $qb = $this->buildQueryByRecentUserHistory($user, $since);
+
+ return $qb->select('count(a)')->getQuery()->getSingleScalarResult();
+ }
+
public function countBy(array $criteria): int
{
return $this->repository->count($criteria);
@@ -54,6 +63,21 @@ final class AccompanyingPeriodRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
+ /**
+ * @return array|AccompanyingPeriod[]
+ */
+ public function findByRecentUserHistory(User $user, DateTimeImmutable $since, ?int $limit = 20, ?int $offset = 0): array
+ {
+ $qb = $this->buildQueryByRecentUserHistory($user, $since);
+
+ return $qb->select('a')
+ ->distinct(true)
+ ->getQuery()
+ ->setMaxResults($limit)
+ ->setFirstResult($offset)
+ ->getResult();
+ }
+
public function findOneBy(array $criteria): ?AccompanyingPeriod
{
return $this->findOneBy($criteria);
@@ -63,4 +87,19 @@ final class AccompanyingPeriodRepository implements ObjectRepository
{
return AccompanyingPeriod::class;
}
+
+ private function buildQueryByRecentUserHistory(User $user, DateTimeImmutable $since): QueryBuilder
+ {
+ $qb = $this->repository->createQueryBuilder('a');
+
+ $qb
+ ->join('a.userHistories', 'userHistory')
+ ->where($qb->expr()->eq('a.user', ':user'))
+ ->andWhere($qb->expr()->gte('userHistory.startDate', ':since'))
+ ->andWhere($qb->expr()->isNull('userHistory.endDate'))
+ ->setParameter('user', $user)
+ ->setParameter('since', $since);
+
+ return $qb;
+ }
}
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss
index e8a98bc80..365074d05 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss
+++ b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss
@@ -80,6 +80,8 @@ div.dashboard {
}
}
div.dashboard,
+h4.badge-title,
+h3.badge-title,
h2.badge-title {
display: flex;
flex-direction: row;
@@ -128,6 +130,8 @@ ul.columns { // XS:1 SM:2 MD:1 LG:2 XL:2 XXL:2
/// dashboard_like_badge in AccompanyingCourse Work list Page
div[class*='accompanying_course_work'] {
div.dashboard,
+ h4.badge-title,
+ h3.badge-title,
h2.badge-title {
span.title_label {
// Calculate same color then border:groove
@@ -143,6 +147,8 @@ div[class*='accompanying_course_work'] {
/// dashboard_like_badge in Activities on resume page
div[class*='activity-'] {
div.dashboard,
+ h4.badge-title,
+ h3.badge-title,
h2.badge-title {
span.title_label {
// Calculate same color then border:groove
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/AddEvaluation.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/AddEvaluation.vue
index 335f9823c..64114aa9f 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/AddEvaluation.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/AddEvaluation.vue
@@ -1,5 +1,6 @@
+
{{ evaluation.evaluation.title.fr }}
diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20220128133039.php b/src/Bundle/ChillPersonBundle/migrations/Version20220128133039.php
new file mode 100644
index 000000000..7b602ba22
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/migrations/Version20220128133039.php
@@ -0,0 +1,51 @@
+addSql('DROP SEQUENCE chill_person_accompanying_period_user_history_id_seq CASCADE');
+ $this->addSql('DROP TABLE chill_person_accompanying_period_user_history');
+ }
+
+ public function getDescription(): string
+ {
+ return 'Add table for tracking user history on accompanying period';
+ }
+
+ public function up(Schema $schema): void
+ {
+ $this->addSql('CREATE SEQUENCE chill_person_accompanying_period_user_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
+ $this->addSql('CREATE TABLE chill_person_accompanying_period_user_history (id INT NOT NULL, user_id INT, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, accompanyingPeriod_id INT, PRIMARY KEY(id))');
+ $this->addSql('CREATE INDEX IDX_6C258C49D7FA8EF0 ON chill_person_accompanying_period_user_history (accompanyingPeriod_id)');
+ $this->addSql('CREATE INDEX IDX_6C258C49A76ED395 ON chill_person_accompanying_period_user_history (user_id)');
+ $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_user_history.endDate IS \'(DC2Type:datetime_immutable)\'');
+ $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_user_history.startDate IS \'(DC2Type:datetime_immutable)\'');
+ $this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD CONSTRAINT FK_6C258C49D7FA8EF0 FOREIGN KEY (accompanyingPeriod_id) REFERENCES chill_person_accompanying_period (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD CONSTRAINT FK_6C258C49A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD CHECK (startdate <= enddate)');
+ $this->addSql('INSERT INTO chill_person_accompanying_period_user_history (id, user_id, accompanyingperiod_id, startDate, endDate) ' .
+ 'SELECT nextval(\'chill_person_accompanying_period_user_history_id_seq\'), user_id, id, openingDate, null FROM chill_person_accompanying_period WHERE user_id IS NOT NULL');
+ $this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL;');
+ $this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD createdBy_id INT DEFAULT NULL;');
+ $this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ALTER user_id SET NOT NULL;');
+ $this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ALTER accompanyingperiod_id SET NOT NULL;');
+ $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_user_history.createdAt IS \'(DC2Type:datetime_immutable)\';');
+ $this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD CONSTRAINT FK_6C258C493174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE;');
+ $this->addSql('CREATE INDEX IDX_6C258C493174800F ON chill_person_accompanying_period_user_history (createdBy_id);');
+ }
+}
diff --git a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php
index 490d50da1..b4b6727b2 100644
--- a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php
+++ b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php
@@ -13,6 +13,8 @@ namespace Chill\TaskBundle\Controller;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
+use Chill\MainBundle\Serializer\Model\Collection;
+use Chill\MainBundle\Serializer\Model\Counter;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
use Chill\MainBundle\Timeline\TimelineBuilder;
@@ -31,6 +33,8 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
+use Symfony\Component\HttpFoundation\Exception\BadRequestException;
+use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@@ -431,10 +435,15 @@ final class SingleTaskController extends AbstractController
* @return Response
* @Route(
* "/{_locale}/task/single-task/list/my",
- * name="chill_task_singletask_my_tasks"
+ * name="chill_task_singletask_my_tasks",
+ * defaults={"_format": "html"}
+ * )
+ * @Route(
+ * "/api/1.0/task/single-task/list/my",
+ * defaults={"_format": "json"}
* )
*/
- public function myTasksAction()
+ public function myTasksAction(string $_format, Request $request)
{
$this->denyAccessUnlessGranted('ROLE_USER');
@@ -447,6 +456,13 @@ final class SingleTaskController extends AbstractController
$filterOrder->getQueryString(),
$flags
);
+
+ if ('json' === $_format && $request->query->getBoolean('countOnly')) {
+ return $this->json(
+ new Counter($nb),
+ );
+ }
+
$paginator = $this->paginatorFactory->create($nb);
$tasks = $this->singleTaskAclAwareRepository->findByCurrentUsersTasks(
$filterOrder->getQueryString(),
@@ -459,11 +475,27 @@ final class SingleTaskController extends AbstractController
]
);
- return $this->render('@ChillTask/SingleTask/List/index_my_tasks.html.twig', [
- 'tasks' => $tasks,
- 'paginator' => $paginator,
- 'filter_order' => $filterOrder,
- ]);
+ switch ($_format) {
+ case 'html':
+ return $this->render('@ChillTask/SingleTask/List/index_my_tasks.html.twig', [
+ 'tasks' => $tasks,
+ 'paginator' => $paginator,
+ 'filter_order' => $filterOrder,
+ ]);
+
+ case 'json':
+ $collection = new Collection($tasks, $paginator);
+
+ return $this->json(
+ $collection,
+ JsonResponse::HTTP_OK,
+ [],
+ ['groups' => ['read']]
+ );
+
+ default:
+ throw new BadRequestException("format not supported: {$_format}");
+ }
}
/**
diff --git a/src/Bundle/ChillTaskBundle/Entity/AbstractTask.php b/src/Bundle/ChillTaskBundle/Entity/AbstractTask.php
index 8b44ebf8f..267bd8bcd 100644
--- a/src/Bundle/ChillTaskBundle/Entity/AbstractTask.php
+++ b/src/Bundle/ChillTaskBundle/Entity/AbstractTask.php
@@ -18,6 +18,7 @@ use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
use function array_fill_keys;
@@ -27,6 +28,9 @@ use function array_keys;
* AbstractTask.
*
* @ORM\MappedSuperclass
+ * @Serializer\DiscriminatorMap(typeProperty="type", mapping={
+ * "single_task": SingleTask::class
+ * })
*/
abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
{
@@ -35,6 +39,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
* @ORM\ManyToOne(
* targetEntity="\Chill\MainBundle\Entity\User"
* )
+ * @Serializer\Groups({"read"})
*/
private $assignee;
@@ -49,12 +54,14 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
/**
* @var bool
* @ORM\Column(name="closed", type="boolean", options={ "default": false })
+ * @Serializer\Groups({"read"})
*/
private $closed = false;
/**
* @var AccompanyingPeriod
* @ORM\ManyToOne(targetEntity="\Chill\PersonBundle\Entity\AccompanyingPeriod")
+ * @Serializer\Groups({"read"})
*/
private $course;
@@ -62,6 +69,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
* @var json
*
* @ORM\Column(name="current_states", type="json")
+ * @Serializer\Groups({"read"})
*/
private $currentStates = [];
@@ -69,6 +77,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
* @var string
*
* @ORM\Column(name="description", type="text")
+ * @Serializer\Groups({"read"})
*/
private $description = '';
@@ -77,6 +86,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
* @ORM\ManyToOne(
* targetEntity="\Chill\PersonBundle\Entity\Person"
* )
+ * @Serializer\Groups({"read"})
*/
private $person;
@@ -85,6 +95,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
*
* @ORM\Column(name="title", type="text")
* @Assert\NotBlank
+ * @Serializer\Groups({"read"})
*/
private $title = '';
@@ -92,6 +103,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
* @var string
*
* @ORM\Column(name="type", type="string", length=255)
+ * @Serializer\Groups({"read"})
*/
private $type;