Merge remote-tracking branch 'origin/master' into issue388_order_social_issues

This commit is contained in:
Julien Fastré 2022-02-11 10:54:37 +01:00
commit 57a88845dc
74 changed files with 1209 additions and 453 deletions

View File

@ -11,16 +11,34 @@ and this project adheres to
## Unreleased ## Unreleased
<!-- write down unreleased development here --> <!-- write down unreleased development here -->
* renommer "dossier numéro" en "parcours numéro" dans les résultats de recherche * change order for accompanying course work list
* renomme date de début en date d'ouverture dans le formulaire parcours * [person]: style fix in parcours listing per person. (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/432)
## Test releases ## Test releases
### test release 2021-01-31 ### test release 2021-02-01
* renommer "dossier numéro" en "parcours numéro" dans les résultats de recherche
* renomme date de début en date d'ouverture dans le formulaire parcours
* [homepage widget] improve content tables, improve counter pluralization with style on number
* [notification lists] add comments counter information
* [workflows] fix popover header with previous transition
* [parcours]: validation + message for closing parcours adjusted.
* [household]: household composition double edit button replaced by a delete action (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/426)
[fast_actions] improve fast-actions buttons override mechanism, fix https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/413 [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. [homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
* [person]: Comment on marital status is possible even if marital status is not defined (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/421)
* [parcours]: In the list of person results the requestor is not displayed if defined as anonymous (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/424)
* [bugfix]: modal closes and newly created person/thirdparty is selected when multiple persons/thirdparties are created through the modal (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/429)
* [person_resource]: Onthefly button added to view person/thirdparty and badge differentiation for a contact-thirdparty (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/428)
* [workflow][notification] improve how notifications and workflows are 'attached' to entities: contextual list, counter, buttons and vue modal
* [AddAddress] disable multiselect search, and rely only on most pertinent Cities and Street computed backend
* [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.
### test release 2021-01-31
* [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: 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: 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) * [person] accompanying course: treat validation error when editing on-the-fly entities (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/420)
@ -31,7 +49,6 @@ and this project adheres to
* [user]: page with accompanying periods to which is user is referent (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/408) * [user]: page with accompanying periods to which is user is referent (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/408)
* [person] age added to renderstring + renderbox/ vue component created to display person text (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/389) * [person] age added to renderstring + renderbox/ vue component created to display person text (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/389)
* [household member editor] allow to push to existing household * [household member editor] allow to push to existing household
* [workflow][notification] improve how notifications and workflows are 'attached' to entities: contextual list, counter, buttons and vue modal
### test release 2021-01-28 ### test release 2021-01-28

View File

@ -34,6 +34,7 @@ use Psr\Log\LoggerInterface;
use RuntimeException; use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -200,12 +201,36 @@ final class ActivityController extends AbstractController
'role' => new Role('CHILL_ACTIVITY_UPDATE'), 'role' => new Role('CHILL_ACTIVITY_UPDATE'),
'activityType' => $entity->getActivityType(), 'activityType' => $entity->getActivityType(),
'accompanyingPeriod' => $accompanyingPeriod, 'accompanyingPeriod' => $accompanyingPeriod,
])->handleRequest($request); ]);
if ($form->has('documents')) {
$form->add('gendocTemplateId', HiddenType::class, [
'mapped' => false,
'data' => null,
'attr' => [
// required for js
'data-template-id' => 'data-template-id',
],
]);
}
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($entity); $this->entityManager->persist($entity);
$this->entityManager->flush(); $this->entityManager->flush();
if ($form->has('gendocTemplateId') && null !== $form['gendocTemplateId']->getData()) {
return $this->redirectToRoute(
'chill_docgenerator_generate_from_template',
[
'template' => $form->get('gendocTemplateId')->getData(),
'entityClassName' => Activity::class,
'entityId' => $entity->getId(),
]
);
}
$this->addFlash('success', $this->get('translator')->trans('Success : activity updated!')); $this->addFlash('success', $this->get('translator')->trans('Success : activity updated!'));
$params = $this->buildParamsToUrl($person, $accompanyingPeriod); $params = $this->buildParamsToUrl($person, $accompanyingPeriod);
@ -393,12 +418,36 @@ final class ActivityController extends AbstractController
'role' => new Role('CHILL_ACTIVITY_CREATE'), 'role' => new Role('CHILL_ACTIVITY_CREATE'),
'activityType' => $entity->getActivityType(), 'activityType' => $entity->getActivityType(),
'accompanyingPeriod' => $accompanyingPeriod, 'accompanyingPeriod' => $accompanyingPeriod,
])->handleRequest($request); ]);
if ($form->has('documents')) {
$form->add('gendocTemplateId', HiddenType::class, [
'mapped' => false,
'data' => null,
'attr' => [
// required for js
'data-template-id' => 'data-template-id',
],
]);
}
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($entity); $this->entityManager->persist($entity);
$this->entityManager->flush(); $this->entityManager->flush();
if ($form->has('gendocTemplateId') && null !== $form['gendocTemplateId']->getData()) {
return $this->redirectToRoute(
'chill_docgenerator_generate_from_template',
[
'template' => $form->get('gendocTemplateId')->getData(),
'entityClassName' => Activity::class,
'entityId' => $entity->getId(),
]
);
}
$this->addFlash('success', $this->get('translator')->trans('Success : activity created!')); $this->addFlash('success', $this->get('translator')->trans('Success : activity created!'));
$params = $this->buildParamsToUrl($person, $accompanyingPeriod); $params = $this->buildParamsToUrl($person, $accompanyingPeriod);

View File

@ -2,11 +2,15 @@ import { createApp } from 'vue';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n' import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'
import { activityMessages } from './i18n' import { activityMessages } from './i18n'
import store from './store' import store from './store'
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
import {fetchTemplates} from 'ChillDocGeneratorAssets/api/pickTemplate.js';
import App from './App.vue'; import App from './App.vue';
const i18n = _createI18n(activityMessages); const i18n = _createI18n(activityMessages);
// app for activity
const hasSocialIssues = document.querySelector('#social-issues-acc') !== null; const hasSocialIssues = document.querySelector('#social-issues-acc') !== null;
const hasLocation = document.querySelector('#location') !== null; const hasLocation = document.querySelector('#location') !== null;
const hasPerson = document.querySelector('#add-persons') !== null; const hasPerson = document.querySelector('#add-persons') !== null;
@ -29,3 +33,54 @@ const app = createApp({
.use(i18n) .use(i18n)
.component('app', App) .component('app', App)
.mount('#activity'); .mount('#activity');
// app for picking template
const i18nGendoc = _createI18n({});
document.querySelectorAll('div[data-docgen-template-picker]').forEach(el => {
fetchTemplates(el.dataset.entityClass).then(templates => {
const picker = {
template:
'<pick-template :templates="this.templates" :preventDefaultMoveToGenerate="true" ' +
':entityClass="faked" @go-to-generate-document="generateDoc"></pick-template>',
components: {
PickTemplate,
},
data() {
return {
templates: templates,
entityId: el.dataset.entityId,
}
},
methods: {
generateDoc({event, link, template}) {
console.log('generateDoc');
console.log('link', link);
console.log('template', template);
let hiddenInput = document.querySelector("input[data-template-id]");
if (hiddenInput === null) {
console.error('hidden input not found');
return;
}
hiddenInput.value = template;
let form = document.querySelector('form[name="chill_activitybundle_activity"');
if (form === null) {
console.error('form not found');
return;
}
form.submit();
}
}
};
createApp(picker).use(i18nGendoc).mount(el);
})
});

View File

@ -68,7 +68,7 @@
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div> <div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
<div class="wl-col list"> <div class="wl-col list">
<p class="wl-item"> <p class="wl-item">
{{ activity.user|chill_entity_render_string|capitalize }} {{ activity.user|chill_entity_render_box }}
</p> </p>
</div> </div>
</div> </div>

View File

@ -89,9 +89,9 @@
{%- if edit_form.documents is defined -%} {%- if edit_form.documents is defined -%}
{{ form_row(edit_form.documents) }} {{ form_row(edit_form.documents) }}
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\ActivityBundle\Entity\Activity" data-entity-id="{{ entity.id }}"></div>
{% endif %} {% endif %}
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\ActivityBundle\Entity\Activity" data-entity-id="{{ entity.id }}"></div>
{% set person_id = null %} {% set person_id = null %}
{% if entity.person %} {% if entity.person %}

View File

@ -24,12 +24,10 @@
window.activity = {{ activity_json|json_encode|raw }}; window.activity = {{ activity_json|json_encode|raw }};
</script> </script>
{{ encore_entry_script_tags('vue_activity') }} {{ encore_entry_script_tags('vue_activity') }}
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
{% endblock %} {% endblock %}
{% block css %} {% block css %}
{{ parent() }} {{ parent() }}
{{ encore_entry_link_tags('mod_async_upload') }} {{ encore_entry_link_tags('mod_async_upload') }}
{{ encore_entry_link_tags('vue_activity') }} {{ encore_entry_link_tags('vue_activity') }}
{{ encore_entry_link_tags('mod_docgen_picktemplate') }}
{% endblock %} {% endblock %}

View File

@ -39,11 +39,9 @@
window.activity = {{ activity_json|json_encode|raw }}; window.activity = {{ activity_json|json_encode|raw }};
</script> </script>
{{ encore_entry_script_tags('vue_activity') }} {{ encore_entry_script_tags('vue_activity') }}
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
{% endblock %} {% endblock %}
{% block css %} {% block css %}
{{ encore_entry_link_tags('mod_async_upload') }} {{ encore_entry_link_tags('mod_async_upload') }}
{{ encore_entry_link_tags('vue_activity') }} {{ encore_entry_link_tags('vue_activity') }}
{{ encore_entry_link_tags('mod_docgen_picktemplate') }}
{% endblock %} {% endblock %}

View File

@ -13,7 +13,7 @@
<p class="date-label">{{ activity.date|format_date('short') }}</p> <p class="date-label">{{ activity.date|format_date('short') }}</p>
{%- endif -%} {%- endif -%}
<span class="like-h3">{{ activity.type.name | localize_translatable_string }}</span> <span class="like-h3">{{ activity.type.name|localize_translatable_string }}</span>
{% if activity.emergency %} {% if activity.emergency %}
<span class="badge bg-danger rounded-pill fs-6">{{ 'Emergency'|trans|upper }}</span> <span class="badge bg-danger rounded-pill fs-6">{{ 'Emergency'|trans|upper }}</span>
@ -41,7 +41,7 @@
{% if activity.user and t.userVisible %} {% if activity.user and t.userVisible %}
<li> <li>
<span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span> <span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span>
<b>{{ activity.user.usernameCanonical }}</b> <b>{{ activity.user|chill_entity_render_box}}</b>
</li> </li>
{% endif %} {% endif %}

View File

@ -87,6 +87,7 @@
{%- if form.documents is defined -%} {%- if form.documents is defined -%}
{{ form_row(form.documents) }} {{ form_row(form.documents) }}
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\ActivityBundle\Entity\Activity" data-entity-id="{{ entity.id }}"></div>
{% endif %} {% endif %}
{%- if form.attendee is defined -%} {%- if form.attendee is defined -%}

View File

@ -17,10 +17,6 @@
{{ parent() }} {{ parent() }}
{{ encore_entry_script_tags('mod_async_upload') }} {{ encore_entry_script_tags('mod_async_upload') }}
<script type="text/javascript"> <script type="text/javascript">
window.addEventListener('DOMContentLoaded', function (e) {
chill.displayAlertWhenLeavingUnsubmittedForm('form[name="{{ form.vars.form.vars.name }}"]',
'{{ "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?"|trans }}');
});
window.activity = {{ activity_json|json_encode|raw }}; window.activity = {{ activity_json|json_encode|raw }};
{% if default_location is not null %}window.default_location_id = {{ default_location.id }}{% endif %}; {% if default_location is not null %}window.default_location_id = {{ default_location.id }}{% endif %};
</script> </script>

View File

@ -17,7 +17,6 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
use Chill\DocGeneratorBundle\Context\Exception\UnexpectedTypeException; use Chill\DocGeneratorBundle\Context\Exception\UnexpectedTypeException;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Service\Context\BaseContextData; use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
use Chill\DocStoreBundle\Entity\Document;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\DocumentCategoryRepository; use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
@ -210,11 +209,8 @@ class ActivityContext implements
*/ */
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
{ {
$doc = new StoredObject(); $entity->addDocument($storedObject);
// TODO push document to remote
$this->em->persist($doc); $this->em->persist($storedObject);
$entity->addDocument($doc);
} }
} }

View File

@ -167,7 +167,6 @@ class AsideActivityCategory
} }
$this->parent = $parent; $this->parent = $parent;
dump($this);
return $this; return $this;
} }

View File

@ -97,8 +97,6 @@ class CalendarType extends AbstractType
return $res; return $res;
}, },
static function (?string $dateAsString): DateTimeImmutable { static function (?string $dateAsString): DateTimeImmutable {
dump($dateAsString);
return new DateTimeImmutable($dateAsString); return new DateTimeImmutable($dateAsString);
} }
)); ));

View File

@ -0,0 +1,13 @@
const buildLink = function(templateId, entityId, entityClass) {
const
entityIdEncoded = encodeURI(entityId),
returnPath = encodeURIComponent(window.location.pathname + window.location.search + window.location.hash),
entityClassEncoded = encodeURI(entityClass),
url = `/fr/doc/gen/generate/from/${templateId}/for/${entityClassEncoded}/${entityIdEncoded}?returnPath=${returnPath}`
;
console.log('computed Url');
return url;
};
export {buildLink};

View File

@ -20,8 +20,8 @@
<option v-bind:value="t.id">{{ t.name.fr || 'Aucun nom défini' }}</option> <option v-bind:value="t.id">{{ t.name.fr || 'Aucun nom défini' }}</option>
</template> </template>
</select> </select>
<button v-if="canGenerate" class="btn btn-update btn-sm change-icon" type="button" @click="generateDocument"><i class="fa fa-fw fa-cog"></i></button> <a v-if="canGenerate" class="btn btn-update btn-sm change-icon" :href="buildUrlGenerate" @click.prevent="clickGenerate($event, buildUrlGenerate)"><i class="fa fa-fw fa-cog"></i></a>
<button v-else class="btn btn-update btn-sm change-icon" type="button" disabled ><i class="fa fa-fw fa-cog"></i></button> <a v-else class="btn btn-update btn-sm change-icon" href="#" disabled ><i class="fa fa-fw fa-cog"></i></a>
</div> </div>
</div> </div>
</div> </div>
@ -39,24 +39,27 @@
<script> <script>
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';
export default { export default {
name: "PickTemplate", name: "PickTemplate",
props: { props: {
entityId: [String, Number], entityId: [String, Number],
entityClass: { entityClass: {
type: String, type: String,
required: true, required: false,
}, },
templates: { templates: {
type: Array, type: Array,
required: true, required: true,
}, },
// beforeMove execute "something" before preventDefaultMoveToGenerate: {
beforeMove: { type: Boolean,
type: Function,
required: false, required: false,
default: false,
} }
}, },
emits: ['goToGenerateDocument'],
data() { data() {
return { return {
template: null, template: null,
@ -74,66 +77,37 @@ export default {
return true; return true;
}, },
getDescription() { getDescription() {
if (null === this.template) {
return '';
}
let desc = this.templates.find(t => t.id === this.template); let desc = this.templates.find(t => t.id === this.template);
if (null === desc) { if (null === desc) {
return ''; return '';
} }
return desc.description || ''; return desc.description || '';
}, },
buildUrlGenerate() {
if (null === this.template) {
return '#';
}
return buildLink(this.template, this.entityId, this.entityClass);
}
}, },
methods: { methods: {
generateDocument() { clickGenerate(event, link) {
console.log('generateDocument'); if (!this.preventDefaultMoveToGenerate) {
console.log('beforeMove', this.beforeMove); window.location.assign(link);
if (this.beforeMove != null) {
console.log('execute before move');
let r = this.beforeMove();
if (r instanceof Promise) {
r.then((obj) => this.goToGenerate(obj));
} else {
this.goToGenerate();
}
} else {
this.goToGenerate();
}
},
goToGenerate(obj) {
console.log('goToGenerate');
console.log('obj', obj);
console.log('entityId', this.entityId);
let
realId = this.entityId,
realEntityClass = this.entityClass
;
if (obj !== undefined) {
if (obj.entityId !== undefined) {
realId = obj.entityId;
}
if (obj.entityClass !== undefined) {
realEntityClass = obj.entityClass;
}
} }
console.log('realId', realId); this.$emit('goToGenerateDocument', {event, link, template: this.template});
console.log('realEntityClass', realEntityClass);
const
entityId = encodeURI(realId),
returnPath = encodeURIComponent(window.location.pathname + window.location.search + window.location.hash),
fqdnEntityClass = encodeURI(realEntityClass),
url = `/fr/doc/gen/generate/from/${this.template}/for/${fqdnEntityClass}/${entityId}?returnPath=${returnPath}`
;
console.log('I will generate your doc at', url);
window.location.assign(url);
}, },
}, },
i18n: { i18n: {
messages: { messages: {
fr: { fr: {
generate_document: 'Générer un document', generate_document: 'Générer un document',
select_a_template: 'Choisir un gabarit', select_a_template: 'Choisir un modèle',
choose_a_template: 'Choisir', choose_a_template: 'Choisir',
} }
} }

View File

@ -8,7 +8,6 @@
</div> </div>
<div class="col-8"> <div class="col-8">
<h3>{{ document.title }}</h3> <h3>{{ document.title }}</h3>
<small>{{ document.object.type }}</small>
{% if document.description is not empty %} {% if document.description is not empty %}
<blockquote class="chill-user-quote mt-2"> <blockquote class="chill-user-quote mt-2">
@ -32,6 +31,13 @@
{% if display_action is defined and display_action == true %} {% if display_action is defined and display_action == true %}
<ul class="record_actions"> <ul class="record_actions">
{% if document.course != null and is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', document.course) %}
<li>
<a href="{{ path('chill_person_accompanying_course_index', {'accompanying_period_id': document.course.id}) }}" class="btn btn-show change-icon">
<i class="fa fa-random"></i> {{ 'Course number'|trans }} {{ document.course.id }}
</a>
</li>
{% endif %}
<li> <li>
{{ m.download_button(document.object, document.title) }} {{ m.download_button(document.object, document.title) }}
</li> </li>
@ -43,19 +49,25 @@
'changeClass' string 'changeClass' string
'noText' boolean 'noText' boolean
#} #}
{# vue component #} {# vue component
<span <span
data-module="wopi-link" data-module="wopi-link"
data-wopi-url="{{ path('chill_wopi_file_edit', {'fileId': document.object.uuid}) }}" data-wopi-url="{{ path('chill_wopi_file_edit', {'fileId': document.object.uuid}) }}"
data-doc-title="{{ document.title|e('html_attr') }}" data-doc-title="{{ document.title|e('html_attr') }}"
data-doc-type="{{ document.object.type|e('html_attr') }}" data-doc-type="{{ document.object.type|e('html_attr') }}"
data-button="{{ button|json_encode }}" data-button="{{ button|json_encode }}"
></span> ></span> #}
<a class="btn btn-update" href="{{ chill_path_add_return_path('chill_wopi_file_edit', {'fileId': document.object.uuid}) }}">{{ 'Edit'|trans }}</a>
{% else %} {% else %}
<a class="btn btn-update change-icon disabled" href="#" title="{{ 'workflow.freezed document'|trans }}"> <a class="btn btn-update change-icon disabled" href="#" title="{{ 'workflow.freezed document'|trans }}">
<i class="fa fa-lock me-2"></i>{{ 'Update document'|trans }} <i class="fa fa-lock me-2"></i>{{ 'Update document'|trans }}
</a> </a>
{% endif %} {% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE', document) and document.course != null %}
<li>
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': document.course.id, 'id': document.id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
</li> </li>
</ul> </ul>
{% endif %} {% endif %}

View File

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

View File

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

View File

@ -110,7 +110,8 @@ class EntityWorkflowRepository implements ObjectRepository
$qb->where( $qb->where(
$qb->expr()->andX( $qb->expr()->andX(
$qb->expr()->isMemberOf(':user', 'step.destUser'), $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; global.chill = chill;
require('./js/date.js'); require('./js/date.js');
require('./js/counter.js');
/// Load fonts /// Load fonts
require('./fonts/OpenSans/OpenSans.scss') 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 // Counter
div.notification-counter { div.notification-counter {
span { span.counter {
&:not(:first-child) { &:not(:first-child) {
&::before { &::before {
content: '/ '; 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

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

View File

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

View File

@ -3,16 +3,38 @@
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span> <span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else> <tab-table v-else>
<template v-slot:thead> <template v-slot:thead>
<th scope="col">id</th> <th scope="col">{{ $t('opening_date') }}</th>
<th scope="col">Ouvert le</th> <th scope="col">{{ $t('social_issues') }}</th>
<th scope="col">Usagers concernés</th> <th scope="col">{{ $t('concerned_persons') }}</th>
<th scope="col"></th>
<th scope="col"></th> <th scope="col"></th>
</template> </template>
<template v-slot:tbody> <template v-slot:tbody>
<tr v-for="(c, i) in accompanyingCourses.results" :key="`course-${i}`"> <tr v-for="(c, i) in accompanyingCourses.results" :key="`course-${i}`">
<td>{{ c.id}}</td> <td>{{ $d(c.openingDate.datetime, 'short') }}</td>
<td>{{ $d(c.openingDate.datetime, 'long') }}</td> <td>
<td>{{ c.participations.length }}</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">{{ $t('emergency') }}</span>
<span v-if="c.confidential" class="badge rounded-pill bg-danger">{{ $t('confidential') }}</span>
</td>
<td> <td>
<a class="btn btn-sm btn-show" :href="getUrl(c)"> <a class="btn btn-sm btn-show" :href="getUrl(c)">
{{ $t('show_entity', { entity: $t('the_course') }) }} {{ $t('show_entity', { entity: $t('the_course') }) }}
@ -26,11 +48,13 @@
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable"; import TabTable from "./TabTable";
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly';
export default { export default {
name: "MyAccompanyingCourses", name: "MyAccompanyingCourses",
components: { components: {
TabTable TabTable,
OnTheFly,
}, },
computed: { computed: {
...mapState([ ...mapState([

View File

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

View File

@ -1,32 +1,71 @@
<template> <template>
<div class="accompanying_course_work">
<div class="alert alert-light">{{ $t('my_evaluations.description') }}</div> <div class="alert alert-light">{{ $t('my_evaluations.description') }}</div>
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span> <span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else> <tab-table v-else>
<template v-slot:thead> <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> <th scope="col"></th>
</template> </template>
<template v-slot:tbody> <template v-slot:tbody>
<tr v-for="(e, i) in evaluations.results" :key="`evaluation-${i}`"> <tr v-for="(e, i) in evaluations.results" :key="`evaluation-${i}`">
<td>{{ e.id}}</td> <td>{{ $d(e.maxDate.datetime, 'short') }}</td>
<td> <td>
{{ 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)"> <a class="btn btn-sm btn-show" :href="getUrl(e)">
{{ $t('show_entity', { entity: $t('the_evaluation') }) }} {{ $t('show_entity', { entity: $t('the_evaluation') }) }}
</a> </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> </td>
</tr> </tr>
</template> </template>
</tab-table> </tab-table>
</div>
</template> </template>
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable"; import TabTable from "./TabTable";
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly';
export default { export default {
name: "MyEvaluations", name: "MyEvaluations",
components: { components: {
TabTable TabTable,
OnTheFly,
}, },
computed: { computed: {
...mapState([ ...mapState([
@ -45,8 +84,17 @@ export default {
}, },
methods: { methods: {
getUrl(e) { getUrl(e) {
switch (e.type) {
case 'accompanying_period_work_evaluation':
let anchor = '#evaluations'; let anchor = '#evaluations';
return `/fr/person/accompanying-period/work/${e.id}/edit${anchor}` 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> <template>
<div class="alert alert-light">{{ $t('my_tasks.description_alert') }}</div> <div class="alert alert-light">{{ $t('my_tasks.description_warning') }}</div>
<span v-if="noResultsWarning" class="chill-no-data-statement">{{ $t('no_data') }}</span> <span v-if="noResultsAlert" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else> <tab-table v-else>
<template v-slot:thead> <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> <th scope="col"></th>
</template> </template>
<template v-slot:tbody> <template v-slot:tbody>
<tr v-for="(t, i) in tasks.warning" :key="`task-warning-${i}`"> <tr v-for="(t, i) in tasks.alert.results" :key="`task-alert-${i}`">
<td>{{ t.id}}</td> <td>{{ $d(t.warningDate.datetime, 'short') }}</td>
<td>
<span class="outdated">{{ $d(t.endDate.datetime, 'short') }}</span>
</td>
<td>{{ t.title }}</td>
<td> <td>
<a class="btn btn-sm btn-show" :href="getUrl(t)"> <a class="btn btn-sm btn-show" :href="getUrl(t)">
{{ $t('show_entity', { entity: $t('the_task') }) }} {{ $t('show_entity', { entity: $t('the_task') }) }}
@ -19,16 +25,22 @@
</template> </template>
</tab-table> </tab-table>
<div class="alert alert-light">{{ $t('my_tasks.description_warning') }}</div> <div class="alert alert-light">{{ $t('my_tasks.description_alert') }}</div>
<span v-if="noResultsAlert" class="chill-no-data-statement">{{ $t('no_data') }}</span> <span v-if="noResultsWarning" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else> <tab-table v-else>
<template v-slot:thead> <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> <th scope="col"></th>
</template> </template>
<template v-slot:tbody> <template v-slot:tbody>
<tr v-for="(t, i) in tasks.alert" :key="`task-alert-${i}`"> <tr v-for="(t, i) in tasks.warning.results" :key="`task-warning-${i}`">
<td>{{ t.id}}</td> <td>
<span class="outdated">{{ $d(t.warningDate.datetime, 'short') }}</span>
</td>
<td>{{ $d(t.endDate.datetime, 'short') }}</td>
<td>{{ t.title }}</td>
<td> <td>
<a class="btn btn-sm btn-show" :href="getUrl(t)"> <a class="btn btn-sm btn-show" :href="getUrl(t)">
{{ $t('show_entity', { entity: $t('the_task') }) }} {{ $t('show_entity', { entity: $t('the_task') }) }}
@ -81,5 +93,8 @@ export default {
</script> </script>
<style scoped> <style scoped>
span.outdated {
font-weight: bold;
color: var(--bs-warning);
}
</style> </style>

View File

@ -6,12 +6,18 @@
<template v-slot:thead> <template v-slot:thead>
<th scope="col">{{ $t('StartDate') }}</th> <th scope="col">{{ $t('StartDate') }}</th>
<th scope="col">{{ $t('SocialAction') }}</th> <th scope="col">{{ $t('SocialAction') }}</th>
<th scope="col">{{ $t('concerned_persons') }}</th>
<th scope="col"></th> <th scope="col"></th>
</template> </template>
<template v-slot:tbody> <template v-slot:tbody>
<tr v-for="(w, i) in works.results" :key="`works-${i}`"> <tr v-for="(w, i) in works.results" :key="`works-${i}`">
<td>{{ $d(w.startDate.datetime, 'short') }}</td> <td>{{ $d(w.startDate.datetime, 'short') }}</td>
<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"> <h4 class="badge-title">
<span class="title_label"></span> <span class="title_label"></span>
<span class="title_action"> <span class="title_action">
@ -20,7 +26,18 @@
</h4> </h4>
</td> </td>
<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)"> <a class="btn btn-sm btn-update" :href="getUrl(w)">
{{ $t('show_entity', { entity: $t('the_action') }) }} {{ $t('show_entity', { entity: $t('the_action') }) }}
</a> </a>
@ -38,11 +55,13 @@
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable"; import TabTable from "./TabTable";
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly';
export default { export default {
name: "MyWorks", name: "MyWorks",
components: { components: {
TabTable TabTable,
OnTheFly,
}, },
computed: { computed: {
...mapState([ ...mapState([

View File

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

View File

@ -22,8 +22,15 @@ const appMessages = {
tab: "Mes notifications", tab: "Mes notifications",
description: "Liste des notifications reçues et non lues.", 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", Date: "Date",
From: "De", From: "Expéditeur",
Subject: "Objet", Subject: "Objet",
Entity: "Associé à", Entity: "Associé à",
show_entity: "Voir {entity}", show_entity: "Voir {entity}",
@ -37,12 +44,12 @@ const appMessages = {
no_data: "Aucun résultats", no_data: "Aucun résultats",
no_dashboard: "Pas de tableaux de bord", no_dashboard: "Pas de tableaux de bord",
counter: { counter: {
unread_notifications: "notifications non lues", unread_notifications: "{n} notification non lue | {n} notifications non lues",
assignated_courses: "parcours récents assignés", assignated_courses: "{n} parcours récent assigné | {n} parcours récents assignés",
assignated_actions: "actions assignées", assignated_actions: "{n} action assignée | {n} actions assignées",
assignated_evaluations: "évaluations assignées", assignated_evaluations: "{n} évaluation assignée | {n} évaluations assignées",
alert_tasks: "tâches en rappel", alert_tasks: "{n} tâche en rappel | {n} tâches en rappel",
warning_tasks: "tâches à échéances", warning_tasks: "{n} tâche à échéance | {n} tâches à échéance",
} }
} }
}; };

View File

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

View File

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

View File

@ -47,7 +47,8 @@ export default {
console.log('goToGenerateWorkflow', event, workflowName); console.log('goToGenerateWorkflow', event, workflowName);
if (!this.$props.preventDefaultMoveToGenerate) { 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)}); this.$emit('goToGenerateWorkflow', {event, workflowName, link: this.makeLink(workflowName)});

View File

@ -91,8 +91,8 @@ export const multiSelectMessages = {
multiselect: { multiselect: {
placeholder: 'Choisir', placeholder: 'Choisir',
tag_placeholder: 'Créer un nouvel élément', tag_placeholder: 'Créer un nouvel élément',
select_label: 'Appuyer sur "Entrée" pour sélectionner', select_label: '"Entrée" ou cliquez pour sélectionner',
deselect_label: 'Appuyer sur "Entrée" pour désélectionner', deselect_label: '"Entrée" ou cliquez pour désélectionner',
select_group_label: 'Appuyer sur "Entrée" pour sélectionner ce groupe', 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', deselect_group_label: 'Appuyer sur "Entrée" pour désélectionner ce groupe',
selected_label: 'Sélectionné' selected_label: 'Sélectionné'

View File

@ -32,6 +32,7 @@
{{ form_start(editedCommentForm) }} {{ form_start(editedCommentForm) }}
{{ form_errors(editedCommentForm) }} {{ form_errors(editedCommentForm) }}
{{ form_widget(editedCommentForm.content) }} {{ form_widget(editedCommentForm.content) }}
{{ form_errors(editedCommentForm.content) }}
<input type="hidden" name="form" value="edit" /> <input type="hidden" name="form" value="edit" />
<ul class="record_actions"> <ul class="record_actions">
<li class="cancel"> <li class="cancel">
@ -64,6 +65,7 @@
{{ form_start(appendCommentForm) }} {{ form_start(appendCommentForm) }}
{{ form_errors(appendCommentForm) }} {{ form_errors(appendCommentForm) }}
{{ form_widget(appendCommentForm.content) }} {{ form_widget(appendCommentForm.content) }}
{{ form_errors(appendCommentForm.content) }}
<input type="hidden" name="form" value="append" /> <input type="hidden" name="form" value="append" />
<ul class="record_actions"> <ul class="record_actions">
<li> <li>

View File

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

View File

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

View File

@ -2,7 +2,7 @@
<h2> <h2>
{{ 'workflow_'|trans }} {{ 'workflow_'|trans }}
</h2> </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, 'description': true,
'breadcrumb': true, 'breadcrumb': true,
'add_classes': 'ms-3 h3' 'add_classes': 'ms-3 h3'

View File

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

View File

@ -44,7 +44,6 @@ class NotificationNormalizer implements NormalizerAwareInterface, NormalizerInte
*/ */
public function normalize($object, ?string $format = null, array $context = []) public function normalize($object, ?string $format = null, array $context = [])
{ {
dump($object);
$entity = $this->entityManager $entity = $this->entityManager
->getRepository($object->getRelatedEntityClass()) ->getRepository($object->getRelatedEntityClass())
->find($object->getRelatedEntityId()); ->find($object->getRelatedEntityId());

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; private EntityWorkflowRepository $entityWorkflowRepository;
public function __construct(EntityWorkflowRepository $entityWorkflowRepository, EntityWorkflowManager $entityWorkflowManager)
{
$this->entityWorkflowRepository = $entityWorkflowRepository;
$this->entityWorkflowManager = $entityWorkflowManager;
}
public function getTemplate(Notification $notification, array $options = []): string public function getTemplate(Notification $notification, array $options = []): string
{ {
return '@ChillMain/Workflow/_notification_include.html.twig'; return '@ChillMain/Workflow/_notification_include.html.twig';

View File

@ -29,3 +29,19 @@ notification:
few {# non-lues} few {# non-lues}
other {# 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: notification:
At least one addressee: Indiquez au moins un destinataire At least one addressee: Indiquez au moins un destinataire
Title must be defined: Un titre doit être indiqué Title must be defined: Un titre doit être indiqué
Comment content might not be blank: Le commentaire ne peut pas être vide

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
@ -23,7 +24,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingCourseWorkController extends AbstractController class AccompanyingCourseWorkController extends AbstractController
{ {
@ -60,7 +61,7 @@ class AccompanyingCourseWorkController extends AbstractController
*/ */
public function createWork(AccompanyingPeriod $period): Response public function createWork(AccompanyingPeriod $period): Response
{ {
// TODO ACL $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::CREATE, $period);
if ($period->getSocialIssues()->count() === 0) { if ($period->getSocialIssues()->count() === 0) {
$this->addFlash( $this->addFlash(
@ -93,7 +94,8 @@ class AccompanyingCourseWorkController extends AbstractController
*/ */
public function deleteWork(AccompanyingPeriodWork $work, Request $request): Response public function deleteWork(AccompanyingPeriodWork $work, Request $request): Response
{ {
// TODO ACL $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::UPDATE, $work);
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
$form = $this->createDeleteForm($work->getId()); $form = $this->createDeleteForm($work->getId());
@ -138,7 +140,8 @@ class AccompanyingCourseWorkController extends AbstractController
*/ */
public function editWork(AccompanyingPeriodWork $work): Response public function editWork(AccompanyingPeriodWork $work): Response
{ {
// TODO ACL $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::UPDATE, $work);
$json = $this->serializer->normalize($work, 'json', ['groups' => ['read']]); $json = $this->serializer->normalize($work, 'json', ['groups' => ['read']]);
return $this->render('@ChillPerson/AccompanyingCourseWork/edit.html.twig', [ return $this->render('@ChillPerson/AccompanyingCourseWork/edit.html.twig', [
@ -157,13 +160,13 @@ class AccompanyingCourseWorkController extends AbstractController
*/ */
public function listWorkByAccompanyingPeriod(AccompanyingPeriod $period): Response public function listWorkByAccompanyingPeriod(AccompanyingPeriod $period): Response
{ {
// TODO ACL $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::SEE, $period);
$totalItems = $this->workRepository->countByAccompanyingPeriod($period); $totalItems = $this->workRepository->countByAccompanyingPeriod($period);
$paginator = $this->paginator->create($totalItems); $paginator = $this->paginator->create($totalItems);
$works = $this->workRepository->findByAccompanyingPeriod( $works = $this->workRepository->findByAccompanyingPeriodOpenFirst(
$period, $period,
['startDate' => 'DESC', 'endDate' => 'DESC'],
$paginator->getItemsPerPage(), $paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber() $paginator->getCurrentPageFirstItemNumber()
); );

View File

@ -99,7 +99,7 @@ class AccompanyingPeriodWorkEvaluationApiController
if ($request->query->getBoolean('countOnly', false)) { if ($request->query->getBoolean('countOnly', false)) {
return new JsonResponse( return new JsonResponse(
$this->serializer->serialize(new Counter($total), 'json', ['groups' => 'read']), $this->serializer->serialize(new Counter($total), 'json', ['groups' => ['read']]),
JsonResponse::HTTP_OK, JsonResponse::HTTP_OK,
[], [],
true true
@ -117,7 +117,7 @@ class AccompanyingPeriodWorkEvaluationApiController
$collection = new Collection($works, $paginator); $collection = new Collection($works, $paginator);
return new JsonResponse( return new JsonResponse(
$this->serializer->serialize($collection, 'json', ['groups' => 'read']), $this->serializer->serialize($collection, 'json', ['groups' => ['read', 'read:evaluation:include-work']]),
JsonResponse::HTTP_OK, JsonResponse::HTTP_OK,
[], [],
true true

View File

@ -16,9 +16,12 @@ use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdComposition; use Chill\PersonBundle\Entity\Household\HouseholdComposition;
use Chill\PersonBundle\Form\HouseholdCompositionType; use Chill\PersonBundle\Form\HouseholdCompositionType;
use Chill\PersonBundle\Repository\Household\HouseholdCompositionRepository; use Chill\PersonBundle\Repository\Household\HouseholdCompositionRepository;
use Chill\PersonBundle\Repository\Household\HouseholdRepository;
use Chill\PersonBundle\Security\Authorization\HouseholdVoter; use Chill\PersonBundle\Security\Authorization\HouseholdVoter;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -31,7 +34,7 @@ use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface; use Symfony\Component\Templating\EngineInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class HouseholdCompositionController class HouseholdCompositionController extends AbstractController
{ {
private EngineInterface $engine; private EngineInterface $engine;
@ -41,6 +44,8 @@ class HouseholdCompositionController
private HouseholdCompositionRepository $householdCompositionRepository; private HouseholdCompositionRepository $householdCompositionRepository;
private HouseholdRepository $householdRepository;
private PaginatorFactory $paginatorFactory; private PaginatorFactory $paginatorFactory;
private Security $security; private Security $security;
@ -52,6 +57,7 @@ class HouseholdCompositionController
public function __construct( public function __construct(
Security $security, Security $security,
HouseholdCompositionRepository $householdCompositionRepository, HouseholdCompositionRepository $householdCompositionRepository,
HouseholdRepository $householdRepository,
PaginatorFactory $paginatorFactory, PaginatorFactory $paginatorFactory,
FormFactoryInterface $formFactory, FormFactoryInterface $formFactory,
EntityManagerInterface $entityManager, EntityManagerInterface $entityManager,
@ -67,6 +73,59 @@ class HouseholdCompositionController
$this->translator = $translator; $this->translator = $translator;
$this->engine = $engine; $this->engine = $engine;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$this->householdRepository = $householdRepository;
}
/**
* @Route("/{_locale}/person/household/{household_id}/composition/{composition_id}/delete", name="chill_person_household_composition_delete")
*
* @param mixed $household_id
* @param mixed $composition_id
*/
public function deleteAction(Request $request, $household_id, $composition_id): Response
{
$composition = $this->householdCompositionRepository->find($composition_id);
$household = $this->householdRepository->find($household_id);
$this->denyAccessUnlessGranted(HouseholdVoter::EDIT, $household);
if (null === $composition) {
throw $this->createNotFoundException('Unable to find composition entity.');
}
$form = $this->createFormBuilder()
->setAction($this->generateUrl('chill_person_household_composition_delete', [
'composition_id' => $composition_id,
'household_id' => $household_id,
]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
if ($request->getMethod() === Request::METHOD_DELETE) {
$form->handleRequest($request);
if ($form->isValid()) {
$this->entityManager->remove($composition);
$this->entityManager->flush();
$this->addFlash('success', $this->translator
->trans('The composition has been successfully removed.'));
return $this->redirectToRoute('chill_person_household_composition_index', [
'id' => $household_id,
]);
}
}
return $this->render(
'ChillPersonBundle:HouseholdComposition:delete.html.twig',
[
'household' => $household,
'composition' => $composition,
'form' => $form->createView(),
]
);
} }
/** /**

View File

@ -133,7 +133,11 @@ class AccompanyingPeriod implements
* @ORM\Column(type="date", nullable=true) * @ORM\Column(type="date", nullable=true)
* @Groups({"read", "write", "docgen:read"}) * @Groups({"read", "write", "docgen:read"})
* @Assert\NotBlank(groups={AccompanyingPeriod::STEP_CLOSED}) * @Assert\NotBlank(groups={AccompanyingPeriod::STEP_CLOSED})
* @Assert\GreaterThan(propertyPath="openingDate", groups={AccompanyingPeriod::STEP_CLOSED}) * @Assert\GreaterThanOrEqual(
* propertyPath="openingDate",
* groups={AccompanyingPeriod::STEP_CLOSED},
* message="The closing date must be later than the date of creation"
* )
*/ */
private ?DateTime $closingDate = null; private ?DateTime $closingDate = null;
@ -213,8 +217,8 @@ class AccompanyingPeriod implements
* *
* @ORM\Column(type="date") * @ORM\Column(type="date")
* @Groups({"read", "write", "docgen:read"}) * @Groups({"read", "write", "docgen:read"})
* @Assert\LessThan(value="today", groups={AccompanyingPeriod::STEP_CONFIRMED}) * @Assert\LessThan(value="tomorrow", groups={AccompanyingPeriod::STEP_CONFIRMED})
* @Assert\LessThan(propertyPath="closingDate", groups={AccompanyingPeriod::STEP_CONFIRMED}) * @Assert\LessThanOrEqual(propertyPath="closingDate", groups={AccompanyingPeriod::STEP_CONFIRMED})
*/ */
private ?DateTime $openingDate = null; private ?DateTime $openingDate = null;

View File

@ -44,7 +44,8 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
{ {
/** /**
* @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class) * @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class)
* @Serializer\Groups({"read"}) * @Serializer\Groups({"read", "read:accompanyingPeriodWork:light"})
* @Serializer\Context(normalizationContext={"groups": {"read"}}, groups={"read:accompanyingPeriodWork:light"})
*/ */
private ?AccompanyingPeriod $accompanyingPeriod = null; private ?AccompanyingPeriod $accompanyingPeriod = null;
@ -63,26 +64,26 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
/** /**
* @ORM\Column(type="datetime_immutable") * @ORM\Column(type="datetime_immutable")
* @Serializer\Groups({"read", "docgen:read"}) * @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
*/ */
private ?DateTimeImmutable $createdAt = null; private ?DateTimeImmutable $createdAt = null;
/** /**
* @ORM\Column(type="boolean") * @ORM\Column(type="boolean")
* @Serializer\Groups({"read", "docgen:read"}) * @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
*/ */
private bool $createdAutomatically = false; private bool $createdAutomatically = false;
/** /**
* @ORM\Column(type="text") * @ORM\Column(type="text")
* @Serializer\Groups({"read", "docgen:read"}) * @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
*/ */
private string $createdAutomaticallyReason = ''; private string $createdAutomaticallyReason = '';
/** /**
* @ORM\ManyToOne(targetEntity=User::class) * @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false) * @ORM\JoinColumn(nullable=false)
* @Serializer\Groups({"read", "docgen:read"}) * @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
*/ */
private ?User $createdBy = null; private ?User $createdBy = null;
@ -90,7 +91,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null}) * @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
* @Serializer\Groups({"accompanying_period_work:create"}) * @Serializer\Groups({"accompanying_period_work:create"})
* @Serializer\Groups({"accompanying_period_work:edit"}) * @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"read", "docgen:read"}) * @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
* @Assert\GreaterThan(propertyPath="startDate", * @Assert\GreaterThan(propertyPath="startDate",
* message="accompanying_course_work.The endDate should be greater than the start date" * message="accompanying_course_work.The endDate should be greater than the start date"
* ) * )
@ -122,7 +123,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(type="integer") * @ORM\Column(type="integer")
* @Serializer\Groups({"read", "docgen:read"}) * @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light", "read:evaluation:include-work"})
*/ */
private ?int $id = null; private ?int $id = null;
@ -135,7 +136,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
/** /**
* @ORM\ManyToMany(targetEntity=Person::class) * @ORM\ManyToMany(targetEntity=Person::class)
* @ORM\JoinTable(name="chill_person_accompanying_period_work_person") * @ORM\JoinTable(name="chill_person_accompanying_period_work_person")
* @Serializer\Groups({"read", "docgen:read"}) * @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
* @Serializer\Groups({"accompanying_period_work:edit"}) * @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"accompanying_period_work:create"}) * @Serializer\Groups({"accompanying_period_work:create"})
*/ */
@ -151,8 +152,9 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
/** /**
* @ORM\ManyToOne(targetEntity=SocialAction::class) * @ORM\ManyToOne(targetEntity=SocialAction::class)
* @Serializer\Groups({"read", "docgen:read"}) * @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
* @Serializer\Groups({"accompanying_period_work:create"}) * @Serializer\Groups({"accompanying_period_work:create"})
* @Serializer\Context(normalizationContext={"groups": {"read"}}, groups={"read:accompanyingPeriodWork:light"})
*/ */
private ?SocialAction $socialAction = null; private ?SocialAction $socialAction = null;
@ -160,7 +162,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
* @ORM\Column(type="date_immutable") * @ORM\Column(type="date_immutable")
* @Serializer\Groups({"accompanying_period_work:create"}) * @Serializer\Groups({"accompanying_period_work:create"})
* @Serializer\Groups({"accompanying_period_work:edit"}) * @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"read", "docgen:read"}) * @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
*/ */
private ?DateTimeImmutable $startDate = null; private ?DateTimeImmutable $startDate = null;

View File

@ -39,6 +39,8 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU
* targetEntity=AccompanyingPeriodWork::class, * targetEntity=AccompanyingPeriodWork::class,
* inversedBy="accompanyingPeriodWorkEvaluations" * inversedBy="accompanyingPeriodWorkEvaluations"
* ) * )
* @Serializer\Groups({"read:evaluation:include-work"})
* @Serializer\Context(normalizationContext={"groups": {"read:accompanyingPeriodWork:light"}}, groups={"read:evaluation:include-work"})
*/ */
private ?AccompanyingPeriodWork $accompanyingPeriodWork = null; private ?AccompanyingPeriodWork $accompanyingPeriodWork = null;

View File

@ -13,7 +13,10 @@ namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Registry;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@ -24,15 +27,18 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
{ {
protected Registry $registry; protected Registry $registry;
protected Security $security;
/** /**
* @var TranslatorInterface * @var TranslatorInterface
*/ */
protected $translator; protected $translator;
public function __construct(TranslatorInterface $translator, Registry $registry) public function __construct(TranslatorInterface $translator, Registry $registry, Security $security)
{ {
$this->translator = $translator; $this->translator = $translator;
$this->registry = $registry; $this->registry = $registry;
$this->security = $security;
} }
public function buildMenu($menuId, MenuItem $menu, array $parameters): void public function buildMenu($menuId, MenuItem $menu, array $parameters): void
@ -47,18 +53,21 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
], ]) ], ])
->setExtras(['order' => 10]); ->setExtras(['order' => 10]);
if ($this->security->isGranted(AccompanyingPeriodVoter::EDIT, $period)) {
$menu->addChild($this->translator->trans('Edit Accompanying Course'), [ $menu->addChild($this->translator->trans('Edit Accompanying Course'), [
'route' => 'chill_person_accompanying_course_edit', 'route' => 'chill_person_accompanying_course_edit',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $period->getId(), 'accompanying_period_id' => $period->getId(),
], ]) ], ])
->setExtras(['order' => 20]); ->setExtras(['order' => 20]);
}
if (AccompanyingPeriod::STEP_DRAFT === $period->getStep()) { if (AccompanyingPeriod::STEP_DRAFT === $period->getStep()) {
// no more menu items if the period is draft // no more menu items if the period is draft
return; return;
} }
if ($this->security->isGranted(AccompanyingPeriodVoter::SEE_DETAILS, $period)) {
$menu->addChild($this->translator->trans('Accompanying Course History'), [ $menu->addChild($this->translator->trans('Accompanying Course History'), [
'route' => 'chill_person_accompanying_course_history', 'route' => 'chill_person_accompanying_course_history',
'routeParameters' => [ 'routeParameters' => [
@ -66,19 +75,22 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
], ]) ], ])
->setExtras(['order' => 30]); ->setExtras(['order' => 30]);
$menu->addChild($this->translator->trans('Accompanying Course Action'), [
'route' => 'chill_person_accompanying_period_work_list',
'routeParameters' => [
'id' => $period->getId(),
], ])
->setExtras(['order' => 40]);
$menu->addChild($this->translator->trans('Accompanying Course Comment'), [ $menu->addChild($this->translator->trans('Accompanying Course Comment'), [
'route' => 'chill_person_accompanying_period_comment_list', 'route' => 'chill_person_accompanying_period_comment_list',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $period->getId(), 'accompanying_period_id' => $period->getId(),
], ]) ], ])
->setExtras(['order' => 50]); ->setExtras(['order' => 50]);
}
if ($this->security->isGranted(AccompanyingPeriodWorkVoter::SEE, $period)) {
$menu->addChild($this->translator->trans('Accompanying Course Action'), [
'route' => 'chill_person_accompanying_period_work_list',
'routeParameters' => [
'id' => $period->getId(),
], ])
->setExtras(['order' => 40]);
}
$workflow = $this->registry->get($period, 'accompanying_period_lifecycle'); $workflow = $this->registry->get($period, 'accompanying_period_lifecycle');

View File

@ -16,17 +16,22 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
final class AccompanyingPeriodWorkRepository implements ObjectRepository final class AccompanyingPeriodWorkRepository implements ObjectRepository
{ {
private EntityManagerInterface $em;
private EntityRepository $repository; private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager)
{ {
$this->em = $entityManager;
$this->repository = $entityManager->getRepository(AccompanyingPeriodWork::class); $this->repository = $entityManager->getRepository(AccompanyingPeriodWork::class);
} }
@ -78,6 +83,36 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
return $this->repository->findByAccompanyingPeriod($period, $orderBy, $limit, $offset); return $this->repository->findByAccompanyingPeriod($period, $orderBy, $limit, $offset);
} }
/**
* Return a list of accompanying period with a defined order:.
*
* * first, opened works
* * then, closed works
*
* @return AccompanyingPeriodWork[]
*/
public function findByAccompanyingPeriodOpenFirst(AccompanyingPeriod $period, int $limit = 10, int $offset = 0): array
{
$rsm = new ResultSetMappingBuilder($this->em);
$rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w');
$sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w
WHERE accompanyingPeriod_id = :periodId
ORDER BY
CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC,
startdate DESC,
enddate DESC,
id DESC
LIMIT :limit OFFSET :offset";
$nq = $this->em->createNativeQuery($sql, $rsm)
->setParameter('periodId', $period->getId(), Types::INTEGER)
->setParameter('limit', $limit, Types::INTEGER)
->setParameter('offset', $offset, Types::INTEGER);
return $nq->getResult();
}
public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array
{ {
return $this->buildQueryNearEndDateByUser($user, $since, $until) return $this->buildQueryNearEndDateByUser($user, $since, $until)

View File

@ -60,14 +60,14 @@
<template v-slot:body> <template v-slot:body>
<p>{{ $t('confirm.sure_description') }}</p> <p>{{ $t('confirm.sure_description') }}</p>
<div v-if="accompanyingCourse.user === null"> <div v-if="accompanyingCourse.user === null">
<div v-if="filteredReferrersSuggested.length === 0"> <div v-if="usersSuggestedFilteredByJob.length === 0">
<p class="alert alert-warning">{{ $t('confirm.no_suggested_referrer') }}</p> <p class="alert alert-warning">{{ $t('confirm.no_suggested_referrer') }}</p>
</div> </div>
<div v-if="filteredReferrersSuggested.length === 1" class="alert alert-info"> <div v-if="usersSuggestedFilteredByJob.length === 1" class="alert alert-info">
<p>{{ $t('confirm.one_suggested_referrer') }}:</p> <p>{{ $t('confirm.one_suggested_referrer') }}:</p>
<ul class="list-suggest add-items inline"> <ul class="list-suggest add-items inline">
<li> <li>
<user-render-box-badge :user="filteredReferrersSuggested[0]"></user-render-box-badge> <user-render-box-badge :user="usersSuggestedFilteredByJob[0]"></user-render-box-badge>
</li> </li>
</ul> </ul>
<p>{{ $t('confirm.choose_suggested_referrer') }}</p> <p>{{ $t('confirm.choose_suggested_referrer') }}</p>
@ -150,7 +150,6 @@ export default {
computed: { computed: {
...mapState({ ...mapState({
accompanyingCourse: state => state.accompanyingCourse, accompanyingCourse: state => state.accompanyingCourse,
filteredReferrersSuggested: state => state.filteredReferrersSuggested
}), }),
...mapGetters([ ...mapGetters([
'isParticipationValid', 'isParticipationValid',
@ -160,15 +159,16 @@ export default {
'isLocationValid', 'isLocationValid',
'isJobValid', 'isJobValid',
'validationKeys', 'validationKeys',
'isValidToBeConfirmed' 'isValidToBeConfirmed',
'usersSuggestedFilteredByJob',
]), ]),
deleteLink() { deleteLink() {
return `/fr/parcours/${this.accompanyingCourse.id}/delete`; //TODO locale return `/fr/parcours/${this.accompanyingCourse.id}/delete`; //TODO locale
}, },
disableConfirm() { disableConfirm() {
return this.clickedDoNotChooseReferrer return (this.accompanyingCourse.user === null
? (this.accompanyingCourse.user === null && this.filteredReferrersSuggested.length === 0) && this.usersSuggestedFilteredByJob.length === 1
: (this.accompanyingCourse.user === null && this.filteredReferrersSuggested.length === 0) || (this.filteredReferrersSuggested.length === 1); && this.clickedDoNotChooseReferrer === false);
} }
}, },
methods: { methods: {
@ -183,7 +183,7 @@ export default {
}); });
}, },
chooseSuggestedReferrer() { chooseSuggestedReferrer() {
this.$store.dispatch('updateReferrer', this.filteredReferrersSuggested[0]) this.$store.dispatch('updateReferrer', this.usersSuggestedFilteredByJob[0])
.catch(({name, violations}) => { .catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') { if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation})); violations.forEach((violation) => this.$toast.open({message: violation}));

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="vue-component"> <div class="vue-component">
<h2><a id="section-80"></a>{{ $t('referrer.title') }}</h2> <h2><a id="section-80"></a>{{ $t('referrer.title') }}</h2>
<div> <div>
@ -21,8 +21,7 @@
:select-label="$t('multiselect.select_label')" :select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')" :deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')" :selected-label="$t('multiselect.selected_label')"
@select="updateJob"> ></VueMultiselect>
</VueMultiselect>
<label class="col-form-label" for="selectReferrer"> <label class="col-form-label" for="selectReferrer">
{{ $t('referrer.label') }} {{ $t('referrer.label') }}
@ -40,12 +39,11 @@
:select-label="$t('multiselect.select_label')" :select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')" :deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')" :selected-label="$t('multiselect.selected_label')"
@select="updateReferrer"> ></VueMultiselect>
</VueMultiselect>
<template v-if="filteredReferrersSuggested.length > 0"> <template v-if="usersSuggestedFilteredByJob.length > 0">
<ul class="list-suggest add-items inline"> <ul class="list-suggest add-items inline">
<li v-for="(u, i) in filteredReferrersSuggested" @click="updateReferrer(u)" :key="`referrer-${i}`"> <li v-for="(u, i) in usersSuggestedFilteredByJob" @click="updateReferrer(u)" :key="`referrer-${i}`">
<span> <span>
<user-render-box-badge :user="u"></user-render-box-badge> <user-render-box-badge :user="u"></user-render-box-badge>
</span> </span>
@ -72,13 +70,13 @@
{{ $t('job.not_valid') }} {{ $t('job.not_valid') }}
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import VueMultiselect from 'vue-multiselect'; import VueMultiselect from 'vue-multiselect';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods'; import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods';
import { mapState, mapGetters } from 'vuex'; import {mapState, mapGetters} from 'vuex';
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge"; import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge";
export default { export default {
@ -94,26 +92,44 @@ export default {
}, },
computed: { computed: {
...mapState({ ...mapState({
value: state => state.accompanyingCourse.user,
valueJob: state => state.accompanyingCourse.job, valueJob: state => state.accompanyingCourse.job,
users: state => state.users.filter(u => { }),
if (u.user_job && state.accompanyingCourse.job) { ...mapGetters(['isJobValid', 'usersSuggestedFilteredByJob']),
return u.user_job.id === state.accompanyingCourse.job.id; users: function () {
} else { let users = this.$store.getters.usersFilteredByJob;
return false;
console.log('users filtered by job', users);
// ensure that the selected user is in the list. add it if necessary
if (this.$store.state.accompanyingCourse.user !== null && users.find(u => this.$store.state.accompanyingCourse.user.id === u.id) === undefined) {
console.log('add user to users');
users.push(this.$store.state.accompanyingCourse.user);
} }
}),
filteredReferrersSuggested: state => state.filteredReferrersSuggested, console.log('users to return', users);
}), return users;
...mapGetters([
'isJobValid'
])
}, },
mounted() { valueJob: {
this.getJobs(); get() {
return this.$store.state.accompanyingCourse.job;
}, },
methods: { set(value) {
updateReferrer(value) { this.$store.dispatch('updateJob', value)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
}
},
value: {
get() {
return this.$store.state.accompanyingCourse.user;
},
set(value) {
console.log('set referrer', value);
this.$store.dispatch('updateReferrer', value) this.$store.dispatch('updateReferrer', value)
.catch(({name, violations}) => { .catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') { if (name === 'ValidationException' || name === 'AccessException') {
@ -122,6 +138,15 @@ export default {
this.$toast.open({message: 'An error occurred'}) this.$toast.open({message: 'An error occurred'})
} }
}); });
}
},
},
mounted() {
this.getJobs();
},
methods: {
updateReferrer(value) {
this.value = value;
}, },
getJobs() { getJobs() {
const url = '/api/1.0/main/user-job.json'; const url = '/api/1.0/main/user-job.json';
@ -133,30 +158,19 @@ export default {
this.$toast.open({message: error.txt}) this.$toast.open({message: error.txt})
}) })
}, },
updateJob(value) {
this.$store.dispatch('updateJob', value)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
},
customJobLabel(value) { customJobLabel(value) {
return value.label.fr; return value.label.fr;
}, },
assignMe() { assignMe() {
const url = `/api/1.0/main/whoami.json`; const url = `/api/1.0/main/whoami.json`;
makeFetch('GET', url) makeFetch('GET', url)
.then(response => { .then(user => {
this.$store.dispatch('updateReferrer', response); this.value = user
return response;
}) })
.catch((error) => { /*.catch((error) => {
commit('catchError', error); commit('catchError', error);
this.$toast.open({message: error.body}) this.$toast.open({message: error.body})
}) })*/
} }
} }
} }

View File

@ -140,7 +140,7 @@ const appMessages = {
sure_description: "Une fois le changement confirmé, il ne sera plus possible de le remettre à l'état de brouillon !", sure_description: "Une fois le changement confirmé, il ne sera plus possible de le remettre à l'état de brouillon !",
ok: "Confirmer le parcours", ok: "Confirmer le parcours",
delete: "Supprimer le parcours", delete: "Supprimer le parcours",
no_suggested_referrer: "Il n'y a aucun référent qui puisse être désigné pour ce parcours. Vérifiez la localisation du parcours, les métiers et service indiqués. Si le problème persiste, contactez l'administrateur du logiciel.", no_suggested_referrer: "Il n'y a aucun référent qui puisse être suggéré pour ce parcours. Vérifiez la localisation du parcours, les métiers et service indiqués. Si les données sont correctes, vous pouvez confirmer ce parcours.",
one_suggested_referrer: "Un unique référent peut être suggéré pour ce parcours", one_suggested_referrer: "Un unique référent peut être suggéré pour ce parcours",
choose_suggested_referrer: "Voulez-vous le désigner directement ?", choose_suggested_referrer: "Voulez-vous le désigner directement ?",
choose_button: "Désigner", choose_button: "Désigner",

View File

@ -40,7 +40,6 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
scopesAtBackend: accompanyingCourse.scopes.map(scope => scope), scopesAtBackend: accompanyingCourse.scopes.map(scope => scope),
// the users which are available for referrer // the users which are available for referrer
referrersSuggested: [], referrersSuggested: [],
filteredReferrersSuggested: [],
// all the users available // all the users available
users: [], users: [],
permissions: {} permissions: {}
@ -93,6 +92,30 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
return false; return false;
}, },
usersFilteredByJob(state) {
return state.users.filter(u => {
if (null !== state.accompanyingCourse.job) {
return (u.user_job === null ? null : u.user_job.id) === state.accompanyingCourse.job.id;
} else {
return true;
}
});
},
usersSuggestedFilteredByJob(state) {
return state.referrersSuggested.filter(u => {
if (null !== state.accompanyingCourse.job) {
return (u.user_job === null ? null : u.user_job.id) === state.accompanyingCourse.job.id;
} else {
return true;
}
}).filter(u => {
if (null !== state.accompanyingCourse.user) {
return u.id !== state.accompanyingCourse.user.id;
}
return true;
});
},
}, },
mutations: { mutations: {
catchError(state, error) { catchError(state, error) {
@ -216,22 +239,6 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
return u; return u;
}); });
}, },
setFilteredReferrersSuggested(state) {
state.filteredReferrersSuggested = state.referrersSuggested.filter(u => {
if (u.user_job && state.accompanyingCourse.job && state.accompanyingCourse.user) {
return u.user_job.id === state.accompanyingCourse.job.id && state.accompanyingCourse.user.id !== u.id
} else {
if (null === state.accompanyingCourse.user) {
if (u.user_job && state.accompanyingCourse.job) {
return u.user_job.id === state.accompanyingCourse.job.id
} else {
return true;
}
}
return state.accompanyingCourse.user.id !== u.id;
}
})
},
confirmAccompanyingCourse(state, response) { confirmAccompanyingCourse(state, response) {
//console.log('### mutation: confirmAccompanyingCourse: response', response); //console.log('### mutation: confirmAccompanyingCourse: response', response);
state.accompanyingCourse.step = response.step; state.accompanyingCourse.step = response.step;
@ -689,8 +696,14 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
}) })
}, },
updateReferrer({ commit }, payload) { updateReferrer({ commit }, payload) {
console.log('update referrer', payload);
console.log('payload !== null', payload !== null);
const url = `/api/1.0/person/accompanying-course/${id}.json`; const url = `/api/1.0/person/accompanying-course/${id}.json`;
const body = { type: "accompanying_period", user: { id: payload.id, type: payload.type }}; let body = { type: "accompanying_period", user: null };
if (payload !== null) {
body = { type: "accompanying_period", user: { id: payload.id, type: payload.type } };
}
return makeFetch('PATCH', url, body) return makeFetch('PATCH', url, body)
.then((response) => { .then((response) => {
@ -704,12 +717,15 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
}, },
updateJob({ commit }, payload) { updateJob({ commit }, payload) {
const url = `/api/1.0/person/accompanying-course/${id}.json`; const url = `/api/1.0/person/accompanying-course/${id}.json`;
const body = { type: "accompanying_period", job: { id: payload.id, type: payload.type }}; let body = { type: "accompanying_period", job: null };
if (payload !== null) {
body = { type: "accompanying_period", job: { id: payload.id, type: payload.type } };
}
return makeFetch('PATCH', url, body) return makeFetch('PATCH', url, body)
.then((response) => { .then((response) => {
commit('updateJob', response.job); commit('updateJob', response.job);
commit('setFilteredReferrersSuggested');
}) })
.catch((error) => { .catch((error) => {
commit('catchError', error); commit('catchError', error);
@ -734,7 +750,6 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
async fetchReferrersSuggested({ state, commit}) { async fetchReferrersSuggested({ state, commit}) {
let users = await getReferrersSuggested(state.accompanyingCourse); let users = await getReferrersSuggested(state.accompanyingCourse);
commit('setReferrersSuggested', users); commit('setReferrersSuggested', users);
commit('setFilteredReferrersSuggested');
if ( if (
null === state.accompanyingCourse.user null === state.accompanyingCourse.user
&& !state.accompanyingCourse.confidential && !state.accompanyingCourse.confidential

View File

@ -92,7 +92,8 @@
entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation" entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation"
:id="evaluation.id" :id="evaluation.id"
:templates="getTemplatesAvailables" :templates="getTemplatesAvailables"
:beforeMove="submitBeforeGenerate" :preventDefaultMoveToGenerate="true"
@go-to-generate-document="submitBeforeGenerate"
> >
<template v-slot:title> <template v-slot:title>
<label class="col-sm-4 col-form-label">{{ $t('evaluation_generate_a_document') }}</label> <label class="col-sm-4 col-form-label">{{ $t('evaluation_generate_a_document') }}</label>
@ -109,6 +110,7 @@ import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js'; import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js';
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue'; import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';
const i18n = { const i18n = {
messages: { messages: {
@ -123,7 +125,7 @@ const i18n = {
evaluation_public_comment: "Note publique", evaluation_public_comment: "Note publique",
evaluation_comment_placeholder: "Commencez à écrire ...", evaluation_comment_placeholder: "Commencez à écrire ...",
evaluation_generate_a_document: "Générer un document", evaluation_generate_a_document: "Générer un document",
evaluation_choose_a_template: "Choisir un gabarit", evaluation_choose_a_template: "Choisir un modèle",
evaluation_add_a_document: "Ajouter un document", evaluation_add_a_document: "Ajouter un document",
evaluation_add: "Ajouter une évaluation", evaluation_add: "Ajouter une évaluation",
Documents: "Documents", Documents: "Documents",
@ -207,10 +209,11 @@ export default {
return `/wopi/edit/${storedObject.uuid}?returnPath=` + encodeURIComponent( return `/wopi/edit/${storedObject.uuid}?returnPath=` + encodeURIComponent(
window.location.pathname + window.location.search + window.location.hash); window.location.pathname + window.location.search + window.location.hash);
}, },
submitBeforeGenerate() { submitBeforeGenerate({template}) {
const callback = (data) => { const callback = (data) => {
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key).id; let evaluationId = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key).id;
return Promise.resolve({entityId: evaluationId});
window.location.assign(buildLink(template, evaluationId, 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation'));
}; };
return this.$store.dispatch('submit', callback).catch(e => { console.log(e); throw e; }); return this.$store.dispatch('submit', callback).catch(e => { console.log(e); throw e; });

View File

@ -241,13 +241,12 @@ export default {
return item.result.type + item.result.id; return item.result.type + item.result.id;
}, },
addPriorSuggestion() { addPriorSuggestion() {
//console.log('addPriorSuggestion', this.hasPriorSuggestion); // console.log('prior suggestion', this.priorSuggestion);
if (this.hasPriorSuggestion) { if (this.hasPriorSuggestion) {
console.log('addPriorSuggestion',); // console.log('addPriorSuggestion',);
this.suggested.unshift(this.priorSuggestion); this.suggested.unshift(this.priorSuggestion);
this.selected.unshift(this.priorSuggestion); this.selected.unshift(this.priorSuggestion);
console.log('reset priorSuggestion');
this.newPriorSuggestion(null); this.newPriorSuggestion(null);
} }
}, },
@ -260,13 +259,14 @@ export default {
result: entity result: entity
} }
this.search.priorSuggestion = suggestion; this.search.priorSuggestion = suggestion;
console.log('search priorSuggestion', this.search.priorSuggestion); // console.log('search priorSuggestion', this.search.priorSuggestion);
this.addPriorSuggestion(suggestion);
} else { } else {
this.search.priorSuggestion = {}; this.search.priorSuggestion = {};
} }
}, },
saveFormOnTheFly({ type, data }) { saveFormOnTheFly({ type, data }) {
console.log('saveFormOnTheFly from addPersons, type', type, ', data', data); // console.log('saveFormOnTheFly from addPersons, type', type, ', data', data);
if (type === 'person') { if (type === 'person') {
makeFetch('POST', '/api/1.0/person/person.json', data) makeFetch('POST', '/api/1.0/person/person.json', data)
.then(response => { .then(response => {
@ -299,6 +299,7 @@ export default {
} }
}) })
} }
this.canCloseOnTheFlyModal = false;
} }
}, },
} }

View File

@ -23,6 +23,8 @@
</div> </div>
{% endif %} {% endif %}
{{ chill_pagination(paginator) }}
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions sticky-form-buttons">
<li> <li>
<a href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_new', { 'id': accompanyingCourse.id }) }}" <a href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_new', { 'id': accompanyingCourse.id }) }}"

View File

@ -41,7 +41,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
<div class="wh-col"> <div class="wh-col" style="align-items: center;">
{% if chill_accompanying_periods.fields.user == 'visible' %} {% if chill_accompanying_periods.fields.user == 'visible' %}
{# the tags `data-referrer-text` is used by module `@ChillPerson/mod/AccompanyingPeriod/setReferrer.js` #} {# the tags `data-referrer-text` is used by module `@ChillPerson/mod/AccompanyingPeriod/setReferrer.js` #}
{% if period.user %} {% if period.user %}

View File

@ -0,0 +1,38 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% set activeRouteKey = 'chill_person_household_composition_index' %}
{% block title 'Remove household composition'|trans %}
{% block display_content %}
<p>{{ 'Concerns household n°%id%'|trans({ '%id%' : household.id } ) }}</p>
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Composition'|trans }}:</h3>
</div>
<div class="wl-col list">
{% for m in household.members %}
<span class="wl-item">
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'person', id: m.person.id },
buttonText: m.person|chill_entity_render_string,
isDead: m.person.deathdate is not null
} %}
</span>
{% endfor %}
</div>
</div>
{% endblock %}
{% block content %}
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'Remove household composition'|trans,
'confirm_question' : 'Are you sure you want to remove this composition?'|trans,
'display_content' : block('display_content'),
'cancel_route' : 'chill_person_household_composition_index',
'cancel_parameters' : { 'composition_id' : composition.id, 'id' : household.id },
'form' : form
} ) }}
{% endblock %}

View File

@ -22,7 +22,7 @@
<h3>{{ c.householdCompositionType.label|localize_translatable_string }}</h3> <h3>{{ c.householdCompositionType.label|localize_translatable_string }}</h3>
<p>{{ 'household_composition.numberOfChildren'|trans }}: {{ c.numberOfChildren }}</p> <p>{{ 'household_composition.numberOfChildren'|trans }}: {{ c.numberOfChildren }}</p>
</div> </div>
<div class="item-col">{{ 'household_composition.Since'|trans({'startDate': c.startDate}) }}</div> <div class="item-col" style="justify-content: flex-end">{{ 'household_composition.Since'|trans({'startDate': c.startDate}) }}</div>
</div> </div>
<div class="item-row"> <div class="item-row">
<div class="item-col"> <div class="item-col">
@ -45,7 +45,10 @@
<a href="{{ path('chill_person_household_composition_index', {'id': c.household.id, 'edit': c.id}) }}" class="btn btn-edit"></a> <a href="{{ path('chill_person_household_composition_index', {'id': c.household.id, 'edit': c.id}) }}" class="btn btn-edit"></a>
</li> </li>
<li> <li>
<a href="{{ path('chill_person_household_composition_index', {'id': c.household.id, 'edit': c.id}) }}" class="btn btn-edit"></a> <a href="{{ chill_path_add_return_path('chill_person_household_composition_delete', {'composition_id': c.id,
'household_id': c.household.id}) }}"
class="btn btn-delete"
title="{{ 'Delete'|trans }}"></a>
</li> </li>
</ul> </ul>
</div> </div>
@ -57,8 +60,8 @@
{{ form_widget(form) }} {{ form_widget(form) }}
<ul class="record_actions"> <ul class="record_actions">
<li class="cancel"> <li class="cancel" style="margin-right: auto;">
<a href="{{ path('chill_person_household_composition_index', {'id': c.household.id}) }}">{{ 'Cancel'|trans }}</a> <a class="btn btn-cancel" href="{{ path('chill_person_household_composition_index', {'id': c.household.id}) }}">{{ 'Cancel'|trans }}</a>
</li> </li>
<li> <li>
<button type="submit" class="btn btn-create">{{ 'Save'|trans }}</button> <button type="submit" class="btn btn-create">{{ 'Save'|trans }}</button>

View File

@ -85,9 +85,11 @@
</div> </div>
<div id="maritalStatusDate"> <div id="maritalStatusDate">
{{ form_row(form.maritalStatusDate, { 'label' : 'Date of last marital status change'} ) }} {{ form_row(form.maritalStatusDate, { 'label' : 'Date of last marital status change'} ) }}
{{ form_row(form.maritalStatusComment, { 'label' : 'Comment on the marital status'} ) }}
</div> </div>
{%- endif -%} {%- endif -%}
<div id="maritalStatusComment">
{{ form_row(form.maritalStatusComment, { 'label' : 'Comment on the marital status'} ) }}
</div>
</fieldset> </fieldset>
{%- endif -%} {%- endif -%}

View File

@ -99,11 +99,12 @@
{% endif %} {% endif %}
</div> </div>
<div class="ms-auto"> <div class="ms-auto">
{% if acp.requestorPerson == person %} {% if acp.requestoranonymous == false and acp.requestorPerson == person %}
<span class="as-requestor badge bg-info" title="{{ 'Requestor'|trans|e('html_attr') }}"> <span class="as-requestor badge bg-info" title="{{ 'Requestor'|trans|e('html_attr') }}">
{{ 'Requestor'|trans({'gender': person.gender}) }} {{ 'Requestor'|trans({'gender': person.gender}) }}
</span> </span>
{% endif %} {% endif %}
{% if acp.emergency %} {% if acp.emergency %}
<span class="badge rounded-pill bg-danger">{{- 'Emergency'|trans|upper -}}</span> <span class="badge rounded-pill bg-danger">{{- 'Emergency'|trans|upper -}}</span>
{% endif %} {% endif %}
@ -178,15 +179,7 @@
</div> </div>
{% endif %} {% endif %}
<ul class="record_actions record_actions_column"> {% if acp.requestoranonymous == false %}
<li>
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': acp.id }) }}"
class="btn btn-sm btn-outline-primary" title="{{ 'See accompanying period'|trans }}">
<i class="fa fa-random fa-fw"></i>
</a>
</li>
</ul>
{% if (acp.requestorPerson is not null and acp.requestorPerson.id != person.id) or acp.requestorThirdParty is not null %} {% if (acp.requestorPerson is not null and acp.requestorPerson.id != person.id) or acp.requestorThirdParty is not null %}
<div class="wl-row"> <div class="wl-row">
<div class="wl-col title"> <div class="wl-col title">
@ -218,6 +211,16 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endif %}
<ul class="record_actions record_actions_column">
<li>
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': acp.id }) }}"
class="btn btn-sm btn-outline-primary" title="{{ 'See accompanying period'|trans }}">
<i class="fa fa-random fa-fw"></i>
</a>
</li>
</ul>
</div> </div>
</div> </div>

View File

@ -23,18 +23,27 @@
{% for resource in personResources %} {% for resource in personResources %}
<div class="item-bloc"> <div class="item-bloc">
<div class="item-row"> <div class="item-row">
<div class="item-col"> <div class="item-col" style="width: 50%">
{% if resource.person is not null %} {% if resource.person is not null %}
<div class="denomination h3"> <div class="denomination h3">
<span class="name">{{ resource.person }}</span> <span>
<span class="badge rounded-pill bg-person">{{ 'person'|trans|capitalize }}</span> {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'person', id: resource.person.id },
buttonText: resource.person|chill_entity_render_string,
isDead: resource.person.deathdate is not null
} %}
</span>
</div> </div>
{% elseif resource.thirdparty is not null %} {% elseif resource.thirdparty is not null %}
<div class="denomination h3"> <div class="denomination h3">
<span class="name">{{ resource.thirdparty }}</span> <span>
<span class="badge rounded-pill bg-thirdparty"> {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
{{ 'thirdparty'|trans|capitalize }} action: 'show', displayBadge: true,
<i class="fa fa-fw fa-user-md"></i> targetEntity: { name: 'thirdparty', id: resource.thirdparty.id },
buttonText: resource.thirdParty|chill_entity_render_string,
parent: resource.thirdparty.parent
} %}
</span> </span>
</div> </div>
{% else %} {% else %}
@ -43,7 +52,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="item-col"> <div class="item-col" style="justify-content: flex-end; ">
{% if resource.kind %} {% if resource.kind %}
<span>{{ resource.kind.title.fr|capitalize }}</span> <span>{{ resource.kind.title.fr|capitalize }}</span>
{% endif %} {% endif %}
@ -56,7 +65,7 @@
</section> </section>
</div> </div>
{% endif %} {% endif %}
{% if is_granted('CHILL_PERSON_UPDATE', resource.person) %} {% if is_granted('CHILL_PERSON_UPDATE', resource.personOwner) %}
<div class="item-row separator"> <div class="item-row separator">
<div class="item-col"> <div class="item-col">
<ul class="record_actions"> <ul class="record_actions">

View File

@ -11,17 +11,23 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Security\Authorization; namespace Chill\PersonBundle\Security\Authorization;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use UnexpectedValueException; use UnexpectedValueException;
use function get_class;
use function in_array; use function in_array;
class AccompanyingPeriodWorkVoter extends Voter class AccompanyingPeriodWorkVoter extends Voter
{ {
public const CREATE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_CREATE';
public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_SEE'; public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_SEE';
public const UPDATE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE';
private Security $security; private Security $security;
public function __construct(Security $security) public function __construct(Security $security)
@ -31,8 +37,14 @@ class AccompanyingPeriodWorkVoter extends Voter
protected function supports($attribute, $subject): bool protected function supports($attribute, $subject): bool
{ {
return $subject instanceof AccompanyingPeriodWork return
&& in_array($attribute, $this->getRoles(), true); (
$subject instanceof AccompanyingPeriodWork
&& in_array($attribute, $this->getRoles(), true)
) || (
$subject instanceof AccompanyingPeriod
&& in_array($attribute, [self::SEE, self::CREATE], true)
);
} }
/** /**
@ -41,13 +53,28 @@ class AccompanyingPeriodWorkVoter extends Voter
*/ */
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{ {
if ($subject instanceof AccompanyingPeriodWork) {
switch ($attribute) { switch ($attribute) {
case self::SEE: case self::SEE:
return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $subject->getAccompanyingPeriod()); return $this->security->isGranted(AccompanyingPeriodVoter::SEE_DETAILS, $subject->getAccompanyingPeriod());
default: default:
throw new UnexpectedValueException("attribute {$attribute} is not supported"); throw new UnexpectedValueException("attribute {$attribute} is not supported");
} }
} elseif ($subject instanceof AccompanyingPeriod) {
switch ($attribute) {
case self::SEE:
return $this->security->isGranted(AccompanyingPeriodVoter::SEE_DETAILS, $subject);
default:
throw new UnexpectedValueException(sprintf(
"attribute {$attribute} is not supported on instance %s",
AccompanyingPeriod::class
));
}
}
throw new UnexpectedValueException(sprintf("attribute {$attribute} on instance %s is not supported", get_class($subject)));
} }
private function getRoles(): array private function getRoles(): array

View File

@ -109,7 +109,6 @@ final class AccompanyingPeriodConfidentialTest extends WebTestCase
$user = new stdClass(); $user = new stdClass();
$user->id = 0; $user->id = 0;
$user->type = 'user'; $user->type = 'user';
dump($user);
$this->client->request( $this->client->request(
Request::METHOD_PATCH, Request::METHOD_PATCH,

View File

@ -36,7 +36,7 @@ class HouseholdMembershipSequentialValidator extends ConstraintValidator
public function validate($person, Constraint $constraint) public function validate($person, Constraint $constraint)
{ {
if (!$person instanceof Person) { if (!$person instanceof Person) {
throw new UnexpectedTypeException($constraint, Person::class); throw new UnexpectedTypeException($person, Person::class);
} }
$participations = $person->getHouseholdParticipationsShareHousehold(); $participations = $person->getHouseholdParticipationsShareHousehold();

View File

@ -148,7 +148,7 @@ and %number% other: '{0} et aucun autre| {1} et une autre |]1, Inf] et %number%
'Last opening since %last_opening%': 'Dernière ouverture le %last_opening%.' 'Last opening since %last_opening%': 'Dernière ouverture le %last_opening%.'
'Person accompanying period - %name%': 'Historique du dossier - %name%' 'Person accompanying period - %name%': 'Historique du dossier - %name%'
'Opening date': 'Date d''ouverture' 'Opening date': 'Date d''ouverture'
'Closing date': 'Date de fermeture' 'Closing date': 'Date de clôture'
'Period opened': 'Période ouverte' 'Period opened': 'Période ouverte'
'Close accompanying period': 'Clôre la période' 'Close accompanying period': 'Clôre la période'
'Open accompanying period': 'Ouvrir la période' 'Open accompanying period': 'Ouvrir la période'
@ -211,7 +211,7 @@ No requestor: Pas de demandeur
No resources: "Pas d'interlocuteurs privilégiés" No resources: "Pas d'interlocuteurs privilégiés"
Persons associated: Usagers concernés Persons associated: Usagers concernés
Referrer: Référent Referrer: Référent
Some peoples does not belong to any household currently. Add them to an household soon: Certaines personnes n'appartiennent à aucun ménage actuellement. Renseignez leur appartenance à un ménage dès que possible. Some peoples does not belong to any household currently. Add them to an household soon: Certaines personnes n'appartiennent à aucun ménage actuellement. Renseignez leur ménage dès que possible.
Add to household now: Ajouter à un ménage Add to household now: Ajouter à un ménage
Any resource for this accompanying course: Aucun interlocuteur privilégié pour ce parcours Any resource for this accompanying course: Aucun interlocuteur privilégié pour ce parcours
course.draft: Brouillon course.draft: Brouillon
@ -237,6 +237,7 @@ no comment found: "Aucun commentaire"
Select a type: "Choisissez un type" Select a type: "Choisissez un type"
Select a person: "Choisissez un usager" Select a person: "Choisissez un usager"
Select a thirdparty: "Choisissez un tiers" Select a thirdparty: "Choisissez un tiers"
Contact person: "Personne de contact"
# pickAPersonType # pickAPersonType
@ -486,6 +487,10 @@ Household summary: Résumé du ménage
Edit household address: Modifier l'adresse du ménage Edit household address: Modifier l'adresse du ménage
Show household: Voir le ménage Show household: Voir le ménage
Back to household: Revenir au ménage Back to household: Revenir au ménage
Remove household composition: Supprimer composition familiale
Are you sure you want to remove this composition?: Etes-vous sûr de vouloir supprimer cette composition familiale ?
Concerns household n°%id%: Concerne le ménage n°%id%
Composition: Composition
# accompanying course work # accompanying course work
Accompanying Course Actions: Actions d'accompagnements Accompanying Course Actions: Actions d'accompagnements
@ -552,7 +557,7 @@ household_composition:
numberOfChildren: Nombre d'enfants mineurs au sein du ménage numberOfChildren: Nombre d'enfants mineurs au sein du ménage
Household composition: Composition du ménage Household composition: Composition du ménage
Composition added: Information sur la composition familiale ajoutée Composition added: Information sur la composition familiale ajoutée
Currently no composition: Aucune composition famiale renseignée. Currently no composition: Aucune composition familiale renseignée.
Add a composition: Ajouter une composition familiale Add a composition: Ajouter une composition familiale
Update composition: Modifier la composition familiale Update composition: Modifier la composition familiale

View File

@ -11,6 +11,7 @@
'Closing date is not valid': 'La date de fermeture n''est pas valide' 'Closing date is not valid': 'La date de fermeture n''est pas valide'
'Closing date can not be null': 'La date de fermeture ne peut être nulle' 'Closing date can not be null': 'La date de fermeture ne peut être nulle'
The date of closing is before the date of opening: La période de fermeture est avant la période d'ouverture The date of closing is before the date of opening: La période de fermeture est avant la période d'ouverture
The closing date must be later than the date of creation: La date de clôture doit être postérieure à la date de création du parcours
The birthdate must be before %date%: La date de naissance doit être avant le %date% The birthdate must be before %date%: La date de naissance doit être avant le %date%
'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33123456789': 'Numéro de téléphone invalide: il doit commencer par le préfixe international précédé de "+", ne comporter que des chiffres et faire moins de 20 caractères. Ex: +31623456789' 'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33123456789': 'Numéro de téléphone invalide: il doit commencer par le préfixe international précédé de "+", ne comporter que des chiffres et faire moins de 20 caractères. Ex: +31623456789'
'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33623456789': 'Numéro de téléphone invalide: il doit commencer par le préfixe international précédé de "+", ne comporter que des chiffres et faire moins de 20 caractères. Ex: +33623456789' 'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33623456789': 'Numéro de téléphone invalide: il doit commencer par le préfixe international précédé de "+", ne comporter que des chiffres et faire moins de 20 caractères. Ex: +33623456789'

View File

@ -17,6 +17,7 @@ use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@ -48,6 +49,7 @@ class SingleTask extends AbstractTask
* *
* @ORM\Column(name="end_date", type="date", nullable=true) * @ORM\Column(name="end_date", type="date", nullable=true)
* @Assert\Date * @Assert\Date
* @Serializer\Groups({"read"})
*/ */
private $endDate; private $endDate;
@ -57,6 +59,7 @@ class SingleTask extends AbstractTask
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO") * @ORM\GeneratedValue(strategy="AUTO")
* @Serializer\Groups({"read"})
*/ */
private $id; private $id;
@ -73,6 +76,7 @@ class SingleTask extends AbstractTask
* @var DateTime * @var DateTime
* *
* @ORM\Column(name="start_date", type="date", nullable=true) * @ORM\Column(name="start_date", type="date", nullable=true)
* @Serializer\Groups({"read"})
* @Assert\Date * @Assert\Date
* *
* @Assert\Expression( * @Assert\Expression(
@ -102,6 +106,7 @@ class SingleTask extends AbstractTask
* and this.getEndDate() === null * and this.getEndDate() === null
* *
* @ORM\Column(name="warning_interval", type="dateinterval", nullable=true) * @ORM\Column(name="warning_interval", type="dateinterval", nullable=true)
* @Serializer\Groups({"read"})
* *
* @Assert\Expression( * @Assert\Expression(
* "!(value != null and this.getEndDate() == null)", * "!(value != null and this.getEndDate() == null)",
@ -162,6 +167,7 @@ class SingleTask extends AbstractTask
* Return null if warningDate or endDate is null * Return null if warningDate or endDate is null
* *
* @return DateTimeImmutable * @return DateTimeImmutable
* @Serializer\Groups({"read"})
*/ */
public function getWarningDate() public function getWarningDate()
{ {