Merge branch 'master' into 'issue427_returnPath'

# Conflicts:
#   CHANGELOG.md
This commit is contained in:
2022-02-11 13:16:40 +00:00
84 changed files with 1313 additions and 497 deletions

View File

@@ -12,6 +12,8 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\Request;
/**
@@ -19,7 +21,7 @@ use Symfony\Component\HttpFoundation\Request;
*/
class LocationApiController extends ApiController
{
public function customizeQuery(string $action, Request $request, $query): void
protected function customizeQuery(string $action, Request $request, $query): void
{
$query
->leftJoin('e.locationType', 'lt')
@@ -31,4 +33,14 @@ class LocationApiController extends ApiController
)
);
}
/**
* @param QueryBuilder $query
* @param mixed $_format
*/
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator, $_format)
{
return $query
->addOrderBy('e.name', 'ASC');
}
}

View File

@@ -236,6 +236,10 @@ class NotificationController extends AbstractController
'_fragment' => 'comment-' . $commentId,
]);
}
if ($editedCommentForm->isSubmitted() && !$editedCommentForm->isValid()) {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
}
}
@@ -257,6 +261,10 @@ class NotificationController extends AbstractController
'id' => $notification->getId(),
]);
}
if ($appendCommentForm->isSubmitted() && !$appendCommentForm->isValid()) {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
}
}

View File

@@ -18,6 +18,7 @@ use DateTimeInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
@@ -28,6 +29,7 @@ class NotificationComment implements TrackCreationInterface, TrackUpdateInterfac
{
/**
* @ORM\Column(type="text")
* @Assert\NotBlank(message="notification.Comment content might not be blank")
*/
private string $content = '';
@@ -136,9 +138,9 @@ class NotificationComment implements TrackCreationInterface, TrackUpdateInterfac
$this->recentlyPersisted = true;
}
public function setContent(string $content): self
public function setContent(?string $content): self
{
$this->content = $content;
$this->content = (string) $content;
return $this;
}

View File

@@ -79,11 +79,8 @@ class NotificationMailer
continue;
}
$email = new Email();
$email
->subject($notification->getTitle());
if ($notification->isSystem()) {
$email = new Email();
$email
->text($notification->getMessage());
} else {
@@ -96,7 +93,9 @@ class NotificationMailer
]);
}
$email->to($addressee->getEmail());
$email
->subject($notification->getTitle())
->to($addressee->getEmail());
try {
$this->mailer->send($email);

View File

@@ -110,7 +110,8 @@ class EntityWorkflowRepository implements ObjectRepository
$qb->where(
$qb->expr()->andX(
$qb->expr()->isMemberOf(':user', 'step.destUser'),
$qb->expr()->isNull('step.transitionAfter')
$qb->expr()->isNull('step.transitionAfter'),
$qb->expr()->eq('step.isFinal', "'FALSE'")
)
);

View File

@@ -0,0 +1,79 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Repository\Workflow;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
class EntityWorkflowStepRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(EntityWorkflowStep::class);
}
public function countUnreadByUser(User $user, ?array $orderBy = null, $limit = null, $offset = null): int
{
$qb = $this->buildQueryByUser($user)->select('count(e)');
return (int) $qb->getQuery()->getSingleScalarResult();
}
public function find($id): ?EntityWorkflowStep
{
return $this->repository->find($id);
}
public function findAll(): array
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?EntityWorkflowStep
{
return $this->repository->findOneBy($criteria);
}
public function getClassName()
{
return EntityWorkflow::class;
}
private function buildQueryByUser(User $user): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('e');
$qb->where(
$qb->expr()->andX(
$qb->expr()->isMemberOf(':user', 'e.destUser'),
$qb->expr()->isNull('e.transitionAt'),
$qb->expr()->eq('e.isFinal', ':bool'),
)
);
$qb->setParameter('user', $user);
$qb->setParameter('bool', false);
return $qb;
}
}

View File

@@ -25,6 +25,7 @@ import { chill } from './js/chill.js';
global.chill = chill;
require('./js/date.js');
require('./js/counter.js');
/// Load fonts
require('./fonts/OpenSans/OpenSans.scss')

View File

@@ -0,0 +1,35 @@
/**
*
* This script search for span.counter elements like
* <span class="counter">Il y a 4 notifications</span>
* and return
* <span class="counter">Il y a <span>4</span> notifications</span>
*
*/
const isNum = (v) => !isNaN(v);
const parseCounter = () => {
document.querySelectorAll('span.counter')
.forEach(el => {
let r = [];
el.innerText
.trim()
.split(' ')
.forEach(w => {
if (isNum(w)) {
r.push(`<span>${w}</span>`);
} else {
r.push(w);
}
})
;
el.innerHTML = r.join(' ');
})
;
};
window.addEventListener('DOMContentLoaded', function (e) {
parseCounter();
});
export { parseCounter };

View File

@@ -82,7 +82,7 @@ div#notification-fold {
// Counter
div.notification-counter {
span {
span.counter {
&:not(:first-child) {
&::before {
content: '/ ';
@@ -90,3 +90,11 @@ div.notification-counter {
}
}
}
span.counter {
& > span {
font-weight: bold;
background-color: $chill-ll-gray;
padding: 0 0.4rem;
border-radius: 50%;
}
}

View File

@@ -17,7 +17,12 @@ function loadDynamicPicker(element) {
isMultiple = parseInt(el.dataset.multiple) === 1,
uniqId = el.dataset.uniqid,
input = element.querySelector('[data-input-uniqid="'+ el.dataset.uniqid +'"]'),
picked = (isMultiple) ? (JSON.parse(input.value)) : ((input.value === '[]') ? (null) : ([JSON.parse(input.value)]));
picked = isMultiple ?
JSON.parse(input.value) : (
(input.value === '[]' || input.value === '') ?
null : [ JSON.parse(input.value) ]
)
;
if (!isMultiple) {
if (input.value === '[]'){

View File

@@ -10,6 +10,7 @@
:deselect-label="$t('create_address')"
:selected-label="$t('multiselect.selected_label')"
@search-change="listenInputSearch"
:internal-search="false"
ref="addressSelector"
@select="selectAddress"
@remove="remove"

View File

@@ -18,6 +18,7 @@
:selected-label="$t('multiselect.selected_label')"
:taggable="true"
:multiple="false"
:internal-search="false"
@tag="addPostcode"
:tagPlaceholder="$t('create_postal_code')"
:loading="isLoading"

View File

@@ -3,16 +3,38 @@
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">id</th>
<th scope="col">Ouvert le</th>
<th scope="col">Usagers concernés</th>
<th scope="col">{{ $t('opening_date') }}</th>
<th scope="col">{{ $t('social_issues') }}</th>
<th scope="col">{{ $t('concerned_persons') }}</th>
<th scope="col"></th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(c, i) in accompanyingCourses.results" :key="`course-${i}`">
<td>{{ c.id}}</td>
<td>{{ $d(c.openingDate.datetime, 'long') }}</td>
<td>{{ c.participations.length }}</td>
<td>{{ $d(c.openingDate.datetime, 'short') }}</td>
<td>
<span v-for="i in c.socialIssues"
class="chill-entity entity-social-issue">
<span class="badge bg-chill-l-gray text-dark">
{{ i.title.fr }}
</span>
</span>
</td>
<td>
<span v-for="p in c.participations" class="me-1" :key="p.person.id">
<on-the-fly
:type="p.person.type"
:id="p.person.id"
:buttonText="p.person.textAge"
:displayBadge="'true' === 'true'"
action="show">
</on-the-fly>
</span>
</td>
<td>
<span v-if="c.emergency" class="badge rounded-pill bg-danger me-1">{{ $t('emergency') }}</span>
<span v-if="c.confidential" class="badge rounded-pill bg-danger">{{ $t('confidential') }}</span>
</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(c)">
{{ $t('show_entity', { entity: $t('the_course') }) }}
@@ -26,11 +48,13 @@
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly';
export default {
name: "MyAccompanyingCourses",
components: {
TabTable
TabTable,
OnTheFly,
},
computed: {
...mapState([
@@ -56,5 +80,7 @@ export default {
</script>
<style scoped>
span.badge.rounded-pill.bg-danger {
text-transform: uppercase;
}
</style>

View File

@@ -6,22 +6,34 @@
<div class="custom1">
<ul class="list-unstyled">
<li v-if="counter.notifications > 0">
<b>{{ counter.notifications }}</b> {{ $t('counter.unread_notifications') }}
<i18n-t keypath="counter.unread_notifications" tag="span" :class="counterClass" :plural="counter.notifications">
<template v-slot:n><span>{{ counter.notifications }}</span></template>
</i18n-t>
</li>
<li v-if="counter.accompanyingCourses > 0">
<b>{{ counter.accompanyingCourses }}</b> {{ $t('counter.assignated_courses') }}
<i18n-t keypath="counter.assignated_courses" tag="span" :class="counterClass" :plural="counter.accompanyingCourses">
<template v-slot:n><span>{{ counter.accompanyingCourses }}</span></template>
</i18n-t>
</li>
<li v-if="counter.works > 0">
<b>{{ counter.works }}</b> {{ $t('counter.assignated_actions') }}
<i18n-t keypath="counter.assignated_actions" tag="span" :class="counterClass" :plural="counter.works">
<template v-slot:n><span>{{ counter.works }}</span></template>
</i18n-t>
</li>
<li v-if="counter.evaluations > 0">
<b>{{ counter.evaluations }}</b> {{ $t('counter.assignated_evaluations') }}
<i18n-t keypath="counter.assignated_evaluations" tag="span" :class="counterClass" :plural="counter.evaluations">
<template v-slot:n><span>{{ counter.evaluations }}</span></template>
</i18n-t>
</li>
<li v-if="counter.tasksAlert > 0">
<b>{{ counter.tasksAlert }}</b> {{ $t('counter.alert_tasks') }}
<i18n-t keypath="counter.alert_tasks" tag="span" :class="counterClass" :plural="counter.tasksAlert">
<template v-slot:n><span>{{ counter.tasksAlert }}</span></template>
</i18n-t>
</li>
<li v-if="counter.tasksWarning > 0">
<b>{{ counter.tasksWarning }}</b> {{ $t('counter.warning_tasks') }}
<i18n-t keypath="counter.warning_tasks" tag="span" :class="counterClass" :plural="counter.tasksWarning">
<template v-slot:n><span>{{ counter.tasksWarning }}</span></template>
</i18n-t>
</li>
</ul>
</div>
@@ -54,6 +66,13 @@ import Masonry from 'masonry-layout/masonry';
export default {
name: "MyCustoms",
data() {
return {
counterClass: {
counter: true //hack to pass class 'counter' in i18n-t
}
}
},
computed: {
...mapGetters(['counter']),
noResults() {
@@ -67,11 +86,16 @@ export default {
}
</script>
<style scoped>
<style lang="scss" scoped>
div.custom4,
div.custom3,
div.custom2 {
font-style: italic;
color: var(--bs-chill-gray);
}
span.counter {
& > span {
background-color: unset;
}
}
</style>

View File

@@ -1,32 +1,71 @@
<template>
<div class="accompanying_course_work">
<div class="alert alert-light">{{ $t('my_evaluations.description') }}</div>
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">id</th>
<th scope="col">{{ $t('max_date') }}</th>
<th scope="col">{{ $t('evaluation') }}</th>
<th scope="col">{{ $t('SocialAction') }}</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(e, i) in evaluations.results" :key="`evaluation-${i}`">
<td>{{ e.id}}</td>
<td>{{ $d(e.maxDate.datetime, 'short') }}</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(e)">
{{ $t('show_entity', { entity: $t('the_evaluation') }) }}
</a>
{{ e.evaluation.title.fr }}
</td>
<td>
<span class="chill-entity entity-social-issue">
<span class="badge bg-chill-l-gray text-dark">
{{ e.accompanyingPeriodWork.socialAction.issue.text }}
</span>
</span>
<h4 class="badge-title">
<span class="title_label"></span>
<span class="title_action">
{{ e.accompanyingPeriodWork.socialAction.text }}
</span>
</h4>
<span v-for="person in e.accompanyingPeriodWork.persons" class="me-1" :key="person.id">
<on-the-fly
:type="person.type"
:id="person.id"
:buttonText="person.textAge"
:displayBadge="'true' === 'true'"
action="show">
</on-the-fly>
</span>
</td>
<td>
<div class="btn-group-vertical" role="group" aria-label="Actions">
<a class="btn btn-sm btn-show" :href="getUrl(e)">
{{ $t('show_entity', { entity: $t('the_evaluation') }) }}
</a>
<a class="btn btn-sm btn-update" :href="getUrl(e.accompanyingPeriodWork)">
{{ $t('show_entity', { entity: $t('the_action') }) }}
</a>
<a class="btn btn-sm btn-show" :href="getUrl(e.accompanyingPeriodWork.accompanyingPeriod)">
{{ $t('show_entity', { entity: $t('the_course') }) }}
</a>
</div>
</td>
</tr>
</template>
</tab-table>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly';
export default {
name: "MyEvaluations",
components: {
TabTable
TabTable,
OnTheFly,
},
computed: {
...mapState([
@@ -45,8 +84,17 @@ export default {
},
methods: {
getUrl(e) {
let anchor = '#evaluations';
return `/fr/person/accompanying-period/work/${e.id}/edit${anchor}`
switch (e.type) {
case 'accompanying_period_work_evaluation':
let anchor = '#evaluations';
return `/fr/person/accompanying-period/work/${e.accompanyingPeriodWork.id}/edit${anchor}`;
case 'accompanying_period_work':
return `/fr/person/accompanying-period/work/${e.id}/edit`
case 'accompanying_period':
return `/fr/parcours/${e.id}`
default:
throw 'entity type unknown';
}
}
},
}

View File

@@ -1,15 +1,21 @@
<template>
<div class="alert alert-light">{{ $t('my_tasks.description_alert') }}</div>
<span v-if="noResultsWarning" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<div class="alert alert-light">{{ $t('my_tasks.description_warning') }}</div>
<span v-if="noResultsAlert" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">id</th>
<th scope="col">{{ $t('warning_date') }}</th>
<th scope="col">{{ $t('max_date') }}</th>
<th scope="col">{{ $t('task') }}</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(t, i) in tasks.warning" :key="`task-warning-${i}`">
<td>{{ t.id}}</td>
<tr v-for="(t, i) in tasks.alert.results" :key="`task-alert-${i}`">
<td>{{ $d(t.warningDate.datetime, 'short') }}</td>
<td>
<span class="outdated">{{ $d(t.endDate.datetime, 'short') }}</span>
</td>
<td>{{ t.title }}</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(t)">
{{ $t('show_entity', { entity: $t('the_task') }) }}
@@ -19,16 +25,22 @@
</template>
</tab-table>
<div class="alert alert-light">{{ $t('my_tasks.description_warning') }}</div>
<span v-if="noResultsAlert" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<div class="alert alert-light">{{ $t('my_tasks.description_alert') }}</div>
<span v-if="noResultsWarning" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">id</th>
<th scope="col">{{ $t('warning_date') }}</th>
<th scope="col">{{ $t('max_date') }}</th>
<th scope="col">{{ $t('task') }}</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(t, i) in tasks.alert" :key="`task-alert-${i}`">
<td>{{ t.id}}</td>
<tr v-for="(t, i) in tasks.warning.results" :key="`task-warning-${i}`">
<td>
<span class="outdated">{{ $d(t.warningDate.datetime, 'short') }}</span>
</td>
<td>{{ $d(t.endDate.datetime, 'short') }}</td>
<td>{{ t.title }}</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(t)">
{{ $t('show_entity', { entity: $t('the_task') }) }}
@@ -81,5 +93,8 @@ export default {
</script>
<style scoped>
span.outdated {
font-weight: bold;
color: var(--bs-warning);
}
</style>

View File

@@ -6,12 +6,18 @@
<template v-slot:thead>
<th scope="col">{{ $t('StartDate') }}</th>
<th scope="col">{{ $t('SocialAction') }}</th>
<th scope="col">{{ $t('concerned_persons') }}</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(w, i) in works.results" :key="`works-${i}`">
<td>{{ $d(w.startDate.datetime, 'short') }}</td>
<td>
<span class="chill-entity entity-social-issue">
<span class="badge bg-chill-l-gray text-dark">
{{ w.socialAction.issue.text }}
</span>
</span>
<h4 class="badge-title">
<span class="title_label"></span>
<span class="title_action">
@@ -20,7 +26,18 @@
</h4>
</td>
<td>
<div class="btn-group" role="group" aria-label="Actions">
<span v-for="person in w.persons" class="me-1" :key="person.id">
<on-the-fly
:type="person.type"
:id="person.id"
:buttonText="person.textAge"
:displayBadge="'true' === 'true'"
action="show">
</on-the-fly>
</span>
</td>
<td>
<div class="btn-group-vertical" role="group" aria-label="Actions">
<a class="btn btn-sm btn-update" :href="getUrl(w)">
{{ $t('show_entity', { entity: $t('the_action') }) }}
</a>
@@ -38,11 +55,13 @@
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly';
export default {
name: "MyWorks",
components: {
TabTable
TabTable,
OnTheFly,
},
computed: {
...mapState([

View File

@@ -1,6 +1,6 @@
<template>
<span v-if="isCounterAvailable"
class="badge rounded-pill bg-danger counter">
class="badge rounded-pill bg-danger">
{{ count }}
</span>
</template>

View File

@@ -22,8 +22,15 @@ const appMessages = {
tab: "Mes notifications",
description: "Liste des notifications reçues et non lues.",
},
opening_date: "Date d'ouverture",
social_issues: "Problématiques sociales",
concerned_persons: "Usagers concernés",
max_date: "Date d'échéance",
warning_date: "Date de rappel",
evaluation: "Évaluation",
task: "Tâche",
Date: "Date",
From: "De",
From: "Expéditeur",
Subject: "Objet",
Entity: "Associé à",
show_entity: "Voir {entity}",
@@ -37,13 +44,15 @@ const appMessages = {
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",
}
unread_notifications: "{n} notification non lue | {n} notifications non lues",
assignated_courses: "{n} parcours récent assigné | {n} parcours récents assignés",
assignated_actions: "{n} action assignée | {n} actions assignées",
assignated_evaluations: "{n} évaluation assignée | {n} évaluations assignées",
alert_tasks: "{n} tâche en rappel | {n} tâches en rappel",
warning_tasks: "{n} tâche à échéance | {n} tâches à échéance",
},
emergency: "Urgent",
confidential: "Confidentiel",
}
};

View File

@@ -62,27 +62,27 @@ const store = createStore({
},
mutations: {
addWorks(state, works) {
console.log('addWorks', works);
//console.log('addWorks', works);
state.works = works;
},
addEvaluations(state, evaluations) {
console.log('addEvaluations', evaluations);
//console.log('addEvaluations', evaluations);
state.evaluations = evaluations;
},
addTasksWarning(state, tasks) {
console.log('addTasksWarning', tasks);
//console.log('addTasksWarning', tasks);
state.tasks.warning = tasks;
},
addTasksAlert(state, tasks) {
console.log('addTasksAlert', tasks);
//console.log('addTasksAlert', tasks);
state.tasks.alert = tasks;
},
addCourses(state, courses) {
console.log('addCourses', courses);
//console.log('addCourses', courses);
state.accompanyingCourses = courses;
},
addNotifications(state, notifications) {
console.log('addNotifications', notifications);
//console.log('addNotifications', notifications);
state.notifications = notifications;
},
setLoading(state, bool) {

View File

@@ -90,7 +90,7 @@ export default {
OnTheFlyThirdparty,
OnTheFlyCreate
},
props: ['type', 'id', 'action', 'buttonText', 'displayBadge', 'isDead', 'parent', 'canCloseModal'],
props: ['type', 'id', 'action', 'buttonText', 'displayBadge', 'isDead', 'parent'],
emits: ['saveFormOnTheFly'],
data() {
return {
@@ -165,16 +165,6 @@ export default {
return `?returnPath=${window.location.pathname}${window.location.search}${window.location.hash}`;
},
},
watch: {
canCloseModal: {
handler: function(val, oldVal) {
if (val) {
this.closeModal();
}
},
deep: true
}
},
methods: {
closeModal() {
this.modal.showModal = false;

View File

@@ -1,4 +1,7 @@
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n';
import { thirdpartyMessages } from 'ChillThirdPartyAssets/vuejs/_js/i18n';
import { addressMessages } from 'ChillMainAssets/vuejs/Address/i18n';
import { ontheflyMessages } from 'ChillMainAssets/vuejs/OnTheFly/i18n';
const appMessages = {
fr: {
@@ -12,6 +15,6 @@ const appMessages = {
}
}
Object.assign(appMessages.fr, personMessages.fr);
Object.assign(appMessages.fr, personMessages.fr, thirdpartyMessages.fr, addressMessages.fr, ontheflyMessages.fr );
export { appMessages };

View File

@@ -93,8 +93,9 @@ export default {
},
getPopTitle(step) {
if (step.transitionPrevious != null) {
//console.log(step.transitionPrevious.text);
let freezed = step.isFreezed ? `<i class="fa fa-snowflake-o fa-sm me-1"></i>` : ``;
return `${freezed}${step.currentStep.text}`;
return `${freezed}${step.transitionPrevious.text}`;
}
},
getPopContent(step) {

View File

@@ -47,7 +47,8 @@ export default {
console.log('goToGenerateWorkflow', event, workflowName);
if (!this.$props.preventDefaultMoveToGenerate) {
event.target.click();
console.log('to go generate');
window.location.assign(this.makeLink(workflowName));
}
this.$emit('goToGenerateWorkflow', {event, workflowName, link: this.makeLink(workflowName)});

View File

@@ -91,11 +91,11 @@ export const multiSelectMessages = {
multiselect: {
placeholder: 'Choisir',
tag_placeholder: 'Créer un nouvel élément',
select_label: 'Appuyer sur "Entrée" pour sélectionner',
deselect_label: 'Appuyer sur "Entrée" pour désélectionner',
select_label: '"Entrée" ou cliquez pour sélectionner',
deselect_label: '"Entrée" ou cliquez pour désélectionner',
select_group_label: 'Appuyer sur "Entrée" pour sélectionner ce groupe',
deselect_group_label: 'Appuyer sur "Entrée" pour désélectionner ce groupe',
selected_label: 'Sélectionné'
}
}
};
};

View File

@@ -15,11 +15,11 @@
<div class="notification-comment-list my-5">
<h2 class="chill-blue">{{ 'notification.comments_list'|trans }}</h2>
{% if notification.comments|length > 0 %}
<div class="flex-table">
{% for comment in notification.comments %}
{% if editedCommentForm is null or editedCommentId != comment.id %}
{{ m.show_comment(comment, {
'recordAction': _self.recordAction(comment)
@@ -28,10 +28,11 @@
<div class="item-bloc">
<div class="item-row row">
<a id="comment-{{ comment.id }}"></a>
{{ form_start(editedCommentForm) }}
{{ form_errors(editedCommentForm) }}
{{ form_widget(editedCommentForm.content) }}
{{ form_errors(editedCommentForm.content) }}
<input type="hidden" name="form" value="edit" />
<ul class="record_actions">
<li class="cancel">
@@ -46,24 +47,25 @@
</li>
</ul>
{{ form_end(editedCommentForm) }}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% else %}
<span class="chill-no-data-statement">{{ 'No comments'|trans }}</span>
{% endif %}
{% if appendCommentForm is not null %}
<div class="new-comment my-5">
<h2 class="chill-blue mb-4">{{ 'Write a new comment'|trans }}</h2>
{{ form_start(appendCommentForm) }}
{{ form_errors(appendCommentForm) }}
{{ form_widget(appendCommentForm.content) }}
{{ form_errors(appendCommentForm.content) }}
<input type="hidden" name="form" value="append" />
<ul class="record_actions">
<li>
@@ -71,7 +73,7 @@
</li>
</ul>
{{ form_end(appendCommentForm) }}
</div>
{% endif %}
</div>
</div>

View File

@@ -72,10 +72,13 @@
<div class="item-row separator">
<div class="item-col item-meta">
{# TODO twig extension to count comments #}
<div class="comment-counter visually-hidden">
<span>x commentaires</span>
</div>
{% if c.notification.comments|length > 0 %}
<div class="comment-counter">
<span class="counter">
{{ 'notification.counter comments'|trans({'nb': c.notification.comments|length }) }}
</span>
</div>
{% endif %}
</div>
<div class="item-col">

View File

@@ -1,11 +1,11 @@
<div class="notification-counter">
{% if counter.total > 0 %}
<span>
<span class="counter">
{{ 'notification.counter total notifications'|trans({'total': counter.total }) }}
</span>
{% endif %}
{% if counter.unread > 0 %}
<span>
<span class="counter">
{{ 'notification.counter unread notifications'|trans({'unread': counter.unread }) }}
</span>
{% endif %}

View File

@@ -2,7 +2,7 @@
<h2>
{{ 'workflow_'|trans }}
</h2>
{% include handler.templateTitle(l.entity_workflow) with handler.templateTitleData(entity_workflow)|merge({
{% include handler.templateTitle(entity_workflow) with handler.templateTitleData(entity_workflow)|merge({
'description': true,
'breadcrumb': true,
'add_classes': 'ms-3 h3'

View File

@@ -14,6 +14,7 @@ namespace Chill\MainBundle\Routing\MenuBuilder;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\Counter\NotificationByUserCounter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Workflow\Counter\WorkflowByUserCounter;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -25,12 +26,16 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
private TranslatorInterface $translator;
private WorkflowByUserCounter $workflowByUserCounter;
public function __construct(
NotificationByUserCounter $notificationByUserCounter,
WorkflowByUserCounter $workflowByUserCounter,
Security $security,
TranslatorInterface $translator
) {
$this->notificationByUserCounter = $notificationByUserCounter;
$this->workflowByUserCounter = $workflowByUserCounter;
$this->security = $security;
$this->translator = $translator;
}
@@ -69,9 +74,11 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
'counter' => $nbNotifications,
]);
$workflowCount = $this->workflowByUserCounter->getCountUnreadByUser($user);
$menu
->addChild(
$this->translator->trans('workflow.My workflows'),
$this->translator->trans('workflow.My workflows with counter', ['wc' => $workflowCount]),
['route' => 'chill_main_workflow_list_dest']
)
->setExtras([

View File

@@ -0,0 +1,98 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Workflow\Counter;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepRepository;
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Workflow\Event\Event;
final class WorkflowByUserCounter implements NotificationCounterInterface, EventSubscriberInterface
{
private CacheItemPoolInterface $cacheItemPool;
private EntityWorkflowStepRepository $workflowStepRepository;
public function __construct(EntityWorkflowStepRepository $workflowStepRepository, CacheItemPoolInterface $cacheItemPool)
{
$this->workflowStepRepository = $workflowStepRepository;
$this->cacheItemPool = $cacheItemPool;
}
public function addNotification(UserInterface $user): int
{
if (!$user instanceof User) {
return 0;
}
$key = self::generateCacheKeyWorkflowByUser($user);
$item = $this->cacheItemPool->getItem($key);
if ($item->isHit()) {
return $item->get();
}
$nb = $this->getCountUnreadByUser($user);
$item->set($nb)
->expiresAfter(60 * 15);
$this->cacheItemPool->save($item);
return $nb;
}
public static function generateCacheKeyWorkflowByUser(User $user): string
{
return 'chill_main_workflow_by_u_' . $user->getId();
}
public function getCountUnreadByUser(User $user): int
{
return $this->workflowStepRepository->countUnreadByUser($user);
}
public static function getSubscribedEvents()
{
return [
'workflow.leave' => 'resetWorkflowCache',
];
}
public function resetWorkflowCache(Event $event): void
{
if (!$event->getSubject() instanceof EntityWorkflow) {
return;
}
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $event->getSubject();
$step = $entityWorkflow->getCurrentStep();
if (null === $step) {
return;
}
$keys = [];
foreach ($step->getDestUser() as $user) {
$keys[] = self::generateCacheKeyWorkflowByUser($user);
}
if ([] !== $keys) {
$this->cacheItemPool->deleteItems($keys);
}
}
}

View File

@@ -23,6 +23,12 @@ class WorkflowNotificationHandler implements NotificationHandlerInterface
private EntityWorkflowRepository $entityWorkflowRepository;
public function __construct(EntityWorkflowRepository $entityWorkflowRepository, EntityWorkflowManager $entityWorkflowManager)
{
$this->entityWorkflowRepository = $entityWorkflowRepository;
$this->entityWorkflowManager = $entityWorkflowManager;
}
public function getTemplate(Notification $notification, array $options = []): string
{
return '@ChillMain/Workflow/_notification_include.html.twig';

View File

@@ -29,3 +29,19 @@ notification:
few {# non-lues}
other {# non-lues}
}
counter comments: >-
{nb, plural,
=0 {Aucun commentaire}
one {# commentaire}
few {# commentaires}
other {# commentaires}
}
workflow:
My workflows with counter: >-
{wc, plural,
=0 {Mes workflows}
one {Une workflow}
few {# workflows}
other {# workflows}
}

View File

@@ -27,4 +27,4 @@ address:
notification:
At least one addressee: Indiquez au moins un destinataire
Title must be defined: Un titre doit être indiqué
Comment content might not be blank: Le commentaire ne peut pas être vide