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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + \ 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 %} - -
-
Appel téléphonique
-
- - {{ chill_widget('homepage', {} ) }} + + + {# 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 @@