Merge branch 'master' into person_renderbox_thirdparty_onthefly

This commit is contained in:
Mathieu Jaumotte 2021-09-14 14:11:33 +02:00
commit 4448547fcd
64 changed files with 1867 additions and 551 deletions

View File

@ -10,7 +10,7 @@
<php> <php>
<ini name="error_reporting" value="-1" /> <ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" /> <server name="APP_ENV" value="test" force="true" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" /> <env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
<server name="SHELL_VERBOSITY" value="-1" /> <server name="SHELL_VERBOSITY" value="-1" />
</php> </php>
@ -31,6 +31,9 @@
<!-- temporarily removed, the time to find a fix --> <!-- temporarily removed, the time to find a fix -->
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude> <exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude>
</testsuite> </testsuite>
<testsuite name="AsideActivityBundle">
<directory suffix="Test.php">src/Bundle/ChillAsideActivityBundle/src/Tests/</directory>
</testsuite>
</testsuites> </testsuites>
<listeners> <listeners>

View File

@ -0,0 +1,20 @@
<?php
namespace Chill\AsideActivityBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
/**
* Controller for activity configuration
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
* @author Champs Libres <info@champs-libres.coop>
*/
class AdminController extends AbstractController
{
public function redirectToAdminIndexAction()
{
return $this->redirectToRoute('chill_main_admin_central');
}
}

View File

@ -1,14 +1,41 @@
<?php <?php
declare(strict_types=1);
namespace Chill\AsideActivityBundle\Controller; namespace Chill\AsideActivityBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController; use Chill\MainBundle\CRUD\Controller\CRUDController;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Doctrine\Common\Collections\Criteria;
final class AsideActivityController extends CRUDController
/**
* Class AsideActivityBundle
*/
class AsideActivityController extends CRUDController
{ {
protected function buildQueryEntities(string $action, Request $request)
{
$qb = parent::buildQueryEntities($action, $request);
if ('index' === $action) {
$qb->andWhere($qb->expr()->eq('e.agent', ':user'));
$qb->setParameter('user', $this->getUser());
}
return $qb;
}
protected function orderQuery(
string $action,
$query,
Request $request,
PaginatorInterface $paginator
) {
if ('index' === $action) {
return $query->orderBy('e.date', 'DESC');
}
return parent::orderQuery($action, $query, $request, $paginator);
}
} }

View File

@ -0,0 +1,53 @@
<?php
namespace Chill\AsideActivityBundle\DataFixtures\ORM;
use Chill\AsideActivityBundle\Entity\AsideActivity;
use Chill\MainBundle\DataFixtures\ORM\LoadUsers;
use Chill\MainBundle\Repository\UserRepository;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
class LoadAsideActivity extends Fixture implements DependentFixtureInterface
{
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function getDependencies(): array
{
return [
LoadUsers::class,
LoadAsideActivityCategory::class
];
}
public function load(ObjectManager $manager)
{
$user = $this->userRepository->findOneBy(['username' => 'center a_social']);
for ($i = 0; $i < 50; $i++) {
$activity = new AsideActivity();
$activity
->setAgent($user)
->setCreatedAt(new \DateTimeImmutable('now'))
->setCreatedBy($user)
->setUpdatedAt(new \DateTimeImmutable('now'))
->setUpdatedBy($user)
->setType(
$this->getReference('aside_activity_category_0')
)
->setDate((new \DateTimeImmutable('today'))
->sub(new \DateInterval('P'.\random_int(1, 100).'D')))
;
$manager->persist($activity);
}
$manager->flush();
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Chill\AsideActivityBundle\DataFixtures\ORM;
use Chill\AsideActivityBundle\Entity\AsideActivityCategory;
use Doctrine\Persistence\ObjectManager;
class LoadAsideActivityCategory extends \Doctrine\Bundle\FixturesBundle\Fixture
{
public function load(ObjectManager $manager)
{
foreach ([
'Appel téléphonique',
'Formation'
] as $key => $label) {
$category = new AsideActivityCategory();
$category->setTitle(['fr' => $label]);
$manager->persist($category);
$this->setReference('aside_activity_category_'.$key, $category);
}
$manager->flush();
}
}

View File

@ -25,8 +25,9 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte
public function load(array $configs, ContainerBuilder $container): void public function load(array $configs, ContainerBuilder $container): void
{ {
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
// $loader->load('services.yaml'); $loader->load('services.yaml');
$loader->load('services/form.yaml'); $loader->load('services/form.yaml');
$loader->load('services/menu.yaml');
} }
public function prepend(ContainerBuilder $container) public function prepend(ContainerBuilder $container)

View File

@ -200,10 +200,4 @@ class AsideActivity implements TrackUpdateInterface, TrackCreationInterface
return $this; return $this;
} }
// public function __toString()
// {
// // dump($this->type->getTitle());
// return $this->type->getTitle();
// }
} }

View File

@ -8,7 +8,9 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
@ -26,8 +28,12 @@ final class AsideActivityFormType extends AbstractType
private TranslatableStringHelper $translatableStringHelper; private TranslatableStringHelper $translatableStringHelper;
private TokenStorageInterface $storage; private TokenStorageInterface $storage;
public function __construct (TranslatableStringHelper $translatableStringHelper, array $timeChoices, TokenStorageInterface $storage){ public function __construct (
$this->timeChoices = $timeChoices; TranslatableStringHelper $translatableStringHelper,
ParameterBagInterface $parameterBag,
TokenStorageInterface $storage
){
$this->timeChoices = $parameterBag->get('chill_activity.form.time_duration');
$this->translatableStringHelper = $translatableStringHelper; $this->translatableStringHelper = $translatableStringHelper;
$this->storage = $storage; $this->storage = $storage;
} }
@ -47,12 +53,15 @@ final class AsideActivityFormType extends AbstractType
]; ];
$builder $builder
->add('agent', EntityType::class, ->add('agent', EntityType::class,
[ [
'label' => 'Agent', 'label' => 'Agent',
'required' => true, 'required' => true,
'class' => User::class, 'class' => User::class,
'data' => $this->storage->getToken()->getUser(), 'data' => $this->storage->getToken()->getUser(),
'query_builder' => function(EntityRepository $er){
return $er->createQueryBuilder('u')->where('u.enabled = true');
},
'attr' => array('class' => 'select2 '), 'attr' => array('class' => 'select2 '),
'placeholder' => 'Choose the agent for whom this activity is created', 'placeholder' => 'Choose the agent for whom this activity is created',
'choice_label' => 'username' 'choice_label' => 'username'
@ -85,7 +94,7 @@ final class AsideActivityFormType extends AbstractType
'required' => false, 'required' => false,
]); ]);
foreach (['duration'] as $fieldName) foreach (['duration'] as $fieldName)
{ {
$builder->get($fieldName) $builder->get($fieldName)
->addModelTransformer($durationTimeTransformer); ->addModelTransformer($durationTimeTransformer);
@ -108,7 +117,6 @@ final class AsideActivityFormType extends AbstractType
$seconds = $data->getTimezone()->getOffset($data); $seconds = $data->getTimezone()->getOffset($data);
$data->setTimeZone($timezoneUTC); $data->setTimeZone($timezoneUTC);
$data->add(new \DateInterval('PT'.$seconds.'S')); $data->add(new \DateInterval('PT'.$seconds.'S'));
dump($data);
// test if the timestamp is in the choices. // test if the timestamp is in the choices.
// If not, recreate the field with the new timestamp // If not, recreate the field with the new timestamp
@ -131,7 +139,7 @@ final class AsideActivityFormType extends AbstractType
public function configureOptions(OptionsResolver $resolver): void public function configureOptions(OptionsResolver $resolver): void
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'data_class' => AsideActivity::class, 'data_class' => AsideActivity::class,
]); ]);
} }
@ -139,4 +147,4 @@ final class AsideActivityFormType extends AbstractType
{ {
return 'chill_asideactivitybundle_asideactivity'; return 'chill_asideactivitybundle_asideactivity';
} }
} }

View File

@ -0,0 +1,46 @@
<?php
namespace Chill\AsideActivityBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Class SectionMenuBuilder
*
* @package Chill\AsideActivityBundle\Menu
*/
class SectionMenuBuilder implements LocalMenuBuilderInterface
{
protected TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* @param $menuId
* @param MenuItem $menu
* @param array $parameters
*/
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
$menu->addChild($this->translator->trans('Create an aside activity'), [
'route' => 'chill_crud_aside_activity_new'
])
->setExtras([
'order' => 11,
'icons' => [ 'plus' ]
]);
}
/**
* @return array
*/
public static function getMenuIds(): array
{
return [ 'section' ];
}
}

View File

@ -0,0 +1,14 @@
{% extends "@ChillMain/Admin/layoutWithVerticalMenu.html.twig" %}
{% block vertical_menu_content %}
{{ chill_menu('admin_aside_activity', {
'layout': '@ChillAsideActivity/Admin/menu_asideactivity.html.twig',
}) }}
{% endblock %}
{% block layout_wvm_content %}
{% block admin_content %}
<!-- block personcontent empty -->
<h1>{{ 'Aside activity configuration' |trans }}</h1>
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,4 @@
{% extends "@ChillMain/Menu/verticalMenu.html.twig" %}
{% block v_menu_title %}
{{ 'Aside activity configuration menu'|trans }}
{% endblock %}

View File

@ -1,37 +1,30 @@
<div class="{% block crud_content_main_div_class %}col-10 centered{% endblock %}"> <div class="{% block crud_content_main_div_class %}col-10 centered{% endblock %}">
{% block crud_content_header %} {% block crud_content_header %}
<h1>{{ ('crud.'~crud_name~'.title_delete')|trans({ '%as_string%': 'Aside Activity' }) }}</h1> <h1>{{ ('crud.'~crud_name~'.title_delete')|trans({ '%as_string%': 'Aside Activity' }) }}</h1>
{% endblock crud_content_header %} {% endblock crud_content_header %}
<p class="message-confirm">{{ ('crud.'~crud_name~'.confirm_message_delete')|trans({ '%as_string%': 'Aside Activity' }) }}</p> <p class="message-confirm">{{ ('crud.'~crud_name~'.confirm_message_delete')|trans({ '%as_string%': 'Aside Activity' }) }}</p>
{{ form_start(form) }} {{ form_start(form) }}
<ul class="record_actions"> <ul class="record_actions">
{% block content_form_actions_back %} {% block content_form_actions_back %}
<li class="cancel"> <li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_index') }}"> <a class="btn btn-cancel" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_index') }}">
{{ 'Cancel'|trans }} {{ 'Cancel'|trans }}
</a> </a>
</li> </li>
{% endblock %} {% endblock %}
{% block content_form_actions_before %}{% endblock %} {% block content_form_actions_before %}
{% block content_form_actions_view %}
{% if is_granted(chill_crud_config('role', crud_name, 'view'), entity) %}
<li class="">
<a class="btn btn-show" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_view', { 'id': entity.id }) }}">
{{ 'crud.edit.back_to_view'|trans }}
</a>
</li>
{% endif %}
{% endblock %}
{% block content_form_actions_confirm_delete %}
<li>
<button type="submit" class="btn btn-delete" value="delete-and-close">{{ ('crud.'~crud_name~'.button_delete')|trans }}</button>
</li>
{% endblock content_form_actions_confirm_delete %}
{% block content_form_actions_after %}{% endblock %}
</ul>
{{ form_end(form) }} {% endblock %}
</div> {% block content_form_actions_confirm_delete %}
<li>
<button type="submit" class="btn btn-delete" value="delete-and-close">{{ ('crud.'~crud_name~'.button_delete')|trans }}</button>
</li>
{% endblock content_form_actions_confirm_delete %}
{% block content_form_actions_after %}{% endblock %}
</ul>
{{ form_end(form) }}
</div>

View File

@ -3,6 +3,6 @@
{# {% block title %}{{ ('crud.' ~ crud_name ~ '.delete.title')|trans({'%crud_name%': crud_name}) }}{% endblock %} #} {# {% block title %}{{ ('crud.' ~ crud_name ~ '.delete.title')|trans({'%crud_name%': crud_name}) }}{% endblock %} #}
{% block content %} {% block content %}
{% embed '@ChillAsideActivity/AsideActivity/_delete.html.twig' %} {% embed '@ChillAsideActivity/asideActivity/_delete.html.twig' %}
{% endembed %} {% endembed %}
{% endblock content %} {% endblock content %}

View File

@ -9,5 +9,6 @@
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %} {% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{# we do not have "view" page. We empty the corresponding block #} {# we do not have "view" page. We empty the corresponding block #}
{% block content_form_actions_view %}{% endblock %} {% block content_form_actions_view %}{% endblock %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %} {% endembed %}
{% endblock %} {% endblock %}

View File

@ -1,107 +1,97 @@
{% extends "@ChillMain/layout.html.twig" %} {% extends "@ChillMain/layout.html.twig" %}
{% block title %}{{ 'Aside activity list' |trans }}{% endblock title %} {% block title %}
{{ 'Aside activity list' |trans }}
{% endblock title %}
{% block content %} {% block content %}
<div class="col-md-10 col-xxl asideactivity-list"> <div class="col-md-10 col-xxl asideactivity-list">
<h2>{{ 'My aside activities' |trans }}</h2> <h2>{{ 'My aside activities' |trans }}</h2>
{% if entities|length == 0 %} {% if entities|length == 0 %}
<p class="chill-no-data-statement"> <p class="chill-no-data-statement">
{{ "There aren't any aside activities."|trans }} {{ "There aren't any aside activities."|trans }}
<a href="{{ path('chill_crud_aside_activity_new') }}" class="btn btn-create button-small"></a> <a href="{{ path('chill_crud_aside_activity_new') }}" class="btn btn-create button-small"></a>
</p> </p>
{% else %} {% else %}
<div class="flex-table my-4 list-records"> <div
{# Sort activities according to date in descending order #} class="flex-table my-4 list-records">
{% for entity in entities|sort ((a, b) => b.date <=> a.date) %} {# Sort activities according to date in descending order #}
{% set t = entity.type %} {% for entity in entities %}
{% set t = entity.type %}
{# only load aside activities of current user. #} <div class="item-bloc">
{% if entity.agent == app.user %} <div class="item-row main">
<div class="item-col">
<div class="item-bloc"> <h3>
<div class="item-row main"> <b>{{ entity.type.title | localize_translatable_string }}</b>
<div class="item-col"> </h3>
<h3> {% if entity.date %}
<b>{{ entity.type.title | localize_translatable_string }}</b> <p>{{ entity.date|format_date('long') }}</p>
</h3> {% endif %}
{% if entity.date %} <div class="duration">
<p>{{ entity.date|format_date('long') }}</p> <p>
{% endif %} <i class="fa fa-fw fa-hourglass-end"></i>
{{ entity.duration|date('H:i') }}
</p>
</div>
<div class="duration"> </div>
<p> <div class="item-col">
<i class="fa fa-fw fa-hourglass-end"></i> <ul class="list-content">
{{ entity.duration|date('H:i') }} {% if entity.createdBy %}
</p> <li>
</div> <b>{{ 'Created by: '|trans }}{{ entity.createdBy.usernameCanonical }}</b>
</li>
{% endif %}
</ul>
</div>
</div>
</div> {# {%
<div class="item-col"> if entity.note is not empty
<ul class="list-content"> or entity.createdBy|length > 0
{% if entity.createdBy %} %}
<li> <div class="item-row details">
<b>{{ 'Created by: '|trans }}{{ entity.createdBy.usernameCanonical }}</b> {% if entity.note is not empty %}
</li> <div class="item-col comment">
{% endif %} {{ entity.note|chill_markdown_to_html }}
</ul> </div>
</div> {% endif %}
</div>
{# {% </div>
if entity.note is not empty {% endif %} #}
or entity.createdBy|length > 0 <div class="item-col">
%} <ul class="list-content">
<div class="item-row details"> <ul class="record_actions">
{% if entity.note is not empty %} <li>
<div class="item-col comment"> <a href="{{ chill_path_add_return_path('chill_crud_aside_activity_edit', { 'id': entity.id }) }}" class="btn btn-update btn-mini "></a>
{{ entity.note|chill_markdown_to_html }} </li>
</div> <li>
{% endif %} <a href="{{ chill_path_add_return_path('chill_crud_aside_activity_delete', { 'id': entity.id } ) }}" class="btn btn-delete btn-mini"></a>
</li>
</ul>
</ul>
</div>
</div>
</div> {% endfor %}
{% endif %} #} </div>
<div class="item-col">
<ul class="list-content"> {{ chill_pagination(paginator) }}
<ul class="record_actions">
{# <li> <ul class="record_actions">
<a href="{{ path('chill_crud_aside_activity_view', { 'id': entity.id} ) }}" class="btn btn-show "></a> <li>
</li> #} <a href="{{ chill_path_add_return_path('chill_crud_aside_activity_new') }}" class="btn btn-create">
{# TOOD {{ 'Create' | trans }}
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %} </a>
#} </li>
<li> </ul>
<a href="{{ path('chill_crud_aside_activity_edit', { 'id': entity.id }) }}" class="btn btn-update "></a> </div>
</li> {% endif %}
{# TOOD
{% endif %}
{% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
#}
<li>
<a href="{{ path('chill_crud_aside_activity_delete', { 'id': entity.id } ) }}" class="btn btn-delete "></a>
</li>
{#
{% endif %}
#}
</ul>
</ul>
</div>
</div>
{% endif %}
{% endfor %}
</div>
<ul class="record_actions">
<li>
<a href="{{ path('chill_crud_aside_activity_new') }}" class="btn btn-create">
{{ 'Add a new aside activity' | trans }}
</a>
</li>
</ul>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,44 +1,44 @@
{% extends "@ChillActivity/Admin/layout_activity.html.twig" %} {% extends "@ChillAsideActivity/Admin/layout_asideactivity.html.twig" %}
{% block admin_content %} {% block admin_content %}
<h1>{{ 'ActivityType list'|trans }}</h1> <h1>{{ 'ActivityType list'|trans }}</h1>
<table class="records_list table table-bordered border-dark"> <table class="records_list table table-bordered border-dark">
<thead> <thead>
<tr> <tr>
<th>{{ 'Name'|trans }}</th> <th>{{ 'Name'|trans }}</th>
<th>{{ 'Active'|trans }}</th> <th>{{ 'Active'|trans }}</th>
<th>{{ 'Actions'|trans }}</th> <th>{{ 'Actions'|trans }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for entity in entities %} {% for entity in entities %}
<tr> <tr>
<td>{{ entity.title|localize_translatable_string }}</td> <td>{{ entity.title|localize_translatable_string }}</td>
<td style="text-align:center;"> <td style="text-align:center;">
{%- if entity.isActive -%} {%- if entity.isActive -%}
<i class="fa fa-check-square-o"></i> <i class="fa fa-check-square-o"></i>
{%- else -%} {%- else -%}
<i class="fa fa-square-o"></i> <i class="fa fa-square-o"></i>
{%- endif -%} {%- endif -%}
</td> </td>
<td> <td>
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions">
<li> <li>
<a href="{{ path('chill_crud_aside_activity_category_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a> <a href="{{ path('chill_crud_aside_activity_category_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li> </li>
</ul> </ul>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
<a href="{{ path('chill_crud_aside_activity_category_new') }}" class="btn btn-create"> <a href="{{ path('chill_crud_aside_activity_category_new') }}" class="btn btn-create">
{{ 'Create a new aside activity type'|trans }} {{ 'Create a new aside activity type'|trans }}
</a> </a>
</li> </li>
</ul> </ul>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,73 @@
<?php
namespace Chill\AsideActivityBundle\Tests\Controller;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Chill\AsideActivityBundle\Entity\AsideActivity;
use Doctrine\ORM\EntityManagerInterface;
class AccompanyingCourseControllerTest extends WebTestCase
{
use PrepareClientTrait;
public function setUp()
{
parent::setUp();
self::bootKernel();
$this->client = $this->getClientAuthenticated();
}
public function testIndexWithoutUsers()
{
$this->client->request('GET', '/fr/asideactivity');
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
public function testNewWithoutUsers()
{
$this->client->request('GET', '/fr/asideactivity/new');
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
/**
* @dataProvider generateAsideActivityId
*/
public function testEditWithoutUsers(int $asideActivityId)
{
$this->client->request('GET', "/fr/asideactivity/{$asideActivityId}/edit");
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
public function generateAsideActivityId()
{
self::bootKernel();
$qb = self::$container->get(EntityManagerInterface::class)
->createQueryBuilder();
$asideActivityIds = $qb
->select('DISTINCT asideactivity.id')
->from(AsideActivity::class, 'asideactivity')
->innerJoin('asideactivity.agent', 'agent')
->where($qb->expr()->eq('agent.username', ':center_name'))
->setParameter('center_name', 'center a_social')
->setMaxResults(100)
->getQuery()
->getResult()
;
\shuffle($asideActivityIds);
yield [ \array_pop($asideActivityIds)['id'] ];
yield [ \array_pop($asideActivityIds)['id'] ];
yield [ \array_pop($asideActivityIds)['id'] ];
}
}

View File

@ -1,3 +1,12 @@
chill_asideactivities_controllers: chill_asideactivities_controllers:
resource: "@ChillAsideActivityBundle/Controller" resource: "@ChillAsideActivityBundle/Controller"
type: annotation type: annotation
chill_admin_aside_activity_redirect_to_admin_index:
path: /{_locale}/admin/activity_redirect_to_main
controller: Chill\ActivityBundle\Controller\AdminController::redirectToAdminIndexAction
options:
menus:
admin_aside_activity:
order: 0
label: Main admin menu

View File

@ -1,8 +1,5 @@
# services: services:
# chill.asideactivity.form.type.asideactivity: Chill\AsideActivityBundle\DataFixtures\:
# class: Chill\AsideActivityBundle\Form\AsideActivityFormType resource: './../DataFixtures'
# arguments: autowire: true
# - "@chill.main.helper.translatable_string" autoconfigure: true
# # - "%chill_activity.form.time_duration%"
# tags:
# - { name: form.type, alias: chill_asideactivitybundle_asideactivity }

View File

@ -1,10 +1,6 @@
--- ---
services: services:
chill.asideactivity.form.type.asideactivity: Chill\AsideActivityBundle\Form\:
class: Chill\AsideActivityBundle\Form\AsideActivityFormType resource: './../../Form'
arguments: autowire: true
- "@chill.main.helper.translatable_string" autoconfigure: true
- "%chill_activity.form.time_duration%"
- "@security.token_storage"
tags:
- { name: form.type, alias: chill_asideactivitybundle_asideactivity }

View File

@ -0,0 +1,5 @@
services:
Chill\AsideActivityBundle\Menu\:
resource: './../../Menu'
autowire: true
autoconfigure: true

View File

@ -1,9 +1,10 @@
#general #general
Show the aside activity: Voir l'activité annexe Show the aside activity: Voir l'activité annexe
Edit the aside activity: Modifier l'activité annexe Edit the aside activity: Modifier l'activité annexe
Remove aside activity: Supprimer l'activité annexe
Aside activity: Activité annexe Aside activity: Activité annexe
Duration time: Durée Duration time: Durée
durationTime: durée durationTime: durée
user_username: nom de l'utilisateur user_username: nom de l'utilisateur
Remark: Commentaire Remark: Commentaire
No comments: Aucun commentaire No comments: Aucun commentaire
@ -25,7 +26,7 @@ Required: Obligatoire
Persons: Personnes Persons: Personnes
Users: Utilisateurs Users: Utilisateurs
Emergency: Urgent Emergency: Urgent
by: 'Par ' by: "Par "
location: Lieu location: Lieu
# Crud # Crud
@ -33,10 +34,10 @@ crud:
aside_activity: aside_activity:
title_view: Détail de l'activité annexe title_view: Détail de l'activité annexe
title_new: Nouvelle activité annexe title_new: Nouvelle activité annexe
title_edit: Edition d'une activité annexe title_edit: Édition d'une activité annexe
title_delete: Supprimation d'une activité annexe title_delete: Supprimer une activité annexe
button_delete: Supprimer button_delete: Supprimer
confirm_message_delete: Êtes-vous sûr de vouloir supprimer cet activité annexe? confirm_message_delete: Êtes-vous sûr de vouloir supprimer cette activité annexe?
aside_activity_category: aside_activity_category:
title_new: Nouvelle catégorie d'activité annexe title_new: Nouvelle catégorie d'activité annexe
title_edit: Edition d'une catégorie de type d'activité title_edit: Edition d'une catégorie de type d'activité
@ -57,26 +58,18 @@ Agent: Utilisateur
date: Date date: Date
Duration: Durée Duration: Durée
Note: Note Note: Note
5 minutes: 5 minutes
10 minutes: 10 minutes
15 minutes: 15 minutes
20 minutes: 20 minutes
25 minutes: 25 minutes
30 minutes: 30 minutes
45 minutes: 45 minutes
1 hour: 1 heure
1 hour 15: 1 heure 15
1 hour 30: 1 heure 30
1 hour 45: 1 heure 45
2 hours: 2 heures
#list #list
My aside activities: Mes activités annexes My aside activities: Mes activités annexes
Date: Date Date: Date
Created by: Creér par Created by: Creér par
#Aside activity delete #Aside activity delete
Delete aside activity: Supprimer une activité annexe Delete aside activity: Supprimer une activité annexe
Are you sure you want to remove the aside activity concerning "%name%" ?: Êtes-vous sûr de vouloir supprimer une activité annexe qui concerne "%name%" ? Are you sure you want to remove the aside activity concerning "%name%" ?: Êtes-vous sûr de vouloir supprimer une activité annexe qui concerne "%name%" ?
The activity has been successfully removed.: L'activité a été supprimée. The activity has been successfully removed.: L'activité a été supprimée.
#Menu
Create an aside activity: "Creér une activité annexe"
Aside activity configuration menu: "Menu de configuration des activités annexes"
Aside activity configuration: "Configuration des activités annexes"

View File

@ -69,7 +69,7 @@ class CalendarController extends AbstractController
/** /**
* Lists all Calendar entities. * Lists all Calendar entities.
* @Route("/{_locale}/calendar/", name="chill_calendar_calendar") * @Route("/{_locale}/calendar/calendar/", name="chill_calendar_calendar_list")
*/ */
public function listAction(Request $request): Response public function listAction(Request $request): Response
{ {
@ -80,18 +80,17 @@ class CalendarController extends AbstractController
if ($user instanceof User) { if ($user instanceof User) {
// $calendar = $em->getRepository(Calendar::class) $calendarItems = $em->getRepository(Calendar::class)
// ->findByUser($user) ->findByUser($user)
// ; ;
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
// $view = 'ChillCalendarBundle:Calendar:listByUser.html.twig';
} elseif ($accompanyingPeriod instanceof AccompanyingPeriod) { } elseif ($accompanyingPeriod instanceof AccompanyingPeriod) {
$calendarItems = $em->getRepository(Calendar::class)->findBy( $calendarItems = $em->getRepository(Calendar::class)->findBy(
['accompanyingPeriod' => $accompanyingPeriod], ['accompanyingPeriod' => $accompanyingPeriod],
['startDate' => 'DESC'] ['startDate' => 'DESC']
); );
$view = 'ChillCalendarBundle:Calendar:listByAccompanyingCourse.html.twig'; $view = '@ChillCalendar/Calendar/listByAccompanyingCourse.html.twig';
} }
return $this->render($view, [ return $this->render($view, [
@ -103,7 +102,7 @@ class CalendarController extends AbstractController
/** /**
* Create a new calendar item * Create a new calendar item
* @Route("/{_locale}/calendar/new", name="chill_calendar_calendar_new") * @Route("/{_locale}/calendar/calendar/new", name="chill_calendar_calendar_new")
*/ */
public function newAction(Request $request): Response public function newAction(Request $request): Response
{ {
@ -112,10 +111,10 @@ class CalendarController extends AbstractController
[$user, $accompanyingPeriod] = $this->getEntity($request); [$user, $accompanyingPeriod] = $this->getEntity($request);
if ($accompanyingPeriod instanceof AccompanyingPeriod) { if ($accompanyingPeriod instanceof AccompanyingPeriod) {
$view = 'ChillCalendarBundle:Calendar:newAccompanyingCourse.html.twig'; $view = '@ChillCalendar/Calendar/newAccompanyingCourse.html.twig';
} }
// elseif ($user instanceof User) { // elseif ($user instanceof User) {
// $view = 'ChillCalendarBundle:Calendar:newUser.html.twig'; // $view = '@ChillCalendar/Calendar/newUser.html.twig';
// } // }
$entity = new Calendar(); $entity = new Calendar();
@ -142,7 +141,7 @@ class CalendarController extends AbstractController
$params = $this->buildParamsToUrl($user, $accompanyingPeriod); $params = $this->buildParamsToUrl($user, $accompanyingPeriod);
return $this->redirectToRoute('chill_calendar_calendar', $params); return $this->redirectToRoute('chill_calendar_calendar_list', $params);
} elseif ($form->isSubmitted() and !$form->isValid()) { } elseif ($form->isSubmitted() and !$form->isValid()) {
$this->addFlash('error', $this->get('translator')->trans('This form contains errors')); $this->addFlash('error', $this->get('translator')->trans('This form contains errors'));
} }
@ -165,7 +164,7 @@ class CalendarController extends AbstractController
/** /**
* Show a calendar item * Show a calendar item
* @Route("/{_locale}/calendar/{id}/show", name="chill_calendar_calendar_show") * @Route("/{_locale}/calendar/calendar/{id}/show", name="chill_calendar_calendar_show")
*/ */
public function showAction(Request $request, $id): Response public function showAction(Request $request, $id): Response
{ {
@ -174,11 +173,11 @@ class CalendarController extends AbstractController
[$user, $accompanyingPeriod] = $this->getEntity($request); [$user, $accompanyingPeriod] = $this->getEntity($request);
if ($accompanyingPeriod instanceof AccompanyingPeriod) { if ($accompanyingPeriod instanceof AccompanyingPeriod) {
$view = 'ChillCalendarBundle:Calendar:showAccompanyingCourse.html.twig'; $view = '@ChillCalendar/Calendar/showByAccompanyingCourse.html.twig';
} }
// elseif ($person instanceof Person) { elseif ($user instanceof User) {
// $view = 'ChillCalendarBundle:Calendar:showPerson.html.twig'; $view = '@ChillCalendar/Calendar/showByUser.html.twig';
// } }
$entity = $em->getRepository('ChillCalendarBundle:Calendar')->find($id); $entity = $em->getRepository('ChillCalendarBundle:Calendar')->find($id);
@ -198,9 +197,9 @@ class CalendarController extends AbstractController
} }
return $this->render($view, [ return $this->render($view, [
//'person' => $person,
'accompanyingCourse' => $accompanyingPeriod, 'accompanyingCourse' => $accompanyingPeriod,
'entity' => $entity, 'entity' => $entity,
'user' => $user
//'delete_form' => $deleteForm->createView(), //'delete_form' => $deleteForm->createView(),
]); ]);
} }
@ -209,7 +208,7 @@ class CalendarController extends AbstractController
/** /**
* Edit a calendar item * Edit a calendar item
* @Route("/{_locale}/calendar/{id}/edit", name="chill_calendar_calendar_edit") * @Route("/{_locale}/calendar/calendar/{id}/edit", name="chill_calendar_calendar_edit")
*/ */
public function editAction($id, Request $request): Response public function editAction($id, Request $request): Response
{ {
@ -218,11 +217,11 @@ class CalendarController extends AbstractController
[$user, $accompanyingPeriod] = $this->getEntity($request); [$user, $accompanyingPeriod] = $this->getEntity($request);
if ($accompanyingPeriod instanceof AccompanyingPeriod) { if ($accompanyingPeriod instanceof AccompanyingPeriod) {
$view = 'ChillCalendarBundle:Calendar:editAccompanyingCourse.html.twig'; $view = '@ChillCalendar/Calendar/editByAccompanyingCourse.html.twig';
}
elseif ($user instanceof User) {
$view = '@ChillCalendar/Calendar/editByUser.html.twig';
} }
// elseif ($person instanceof Person) {
// $view = 'ChillCalendarBundle:Calendar:editPerson.html.twig';
// }
$entity = $em->getRepository('ChillCalendarBundle:Calendar')->find($id); $entity = $em->getRepository('ChillCalendarBundle:Calendar')->find($id);
@ -241,7 +240,7 @@ class CalendarController extends AbstractController
$this->addFlash('success', $this->get('translator')->trans('Success : calendar item updated!')); $this->addFlash('success', $this->get('translator')->trans('Success : calendar item updated!'));
$params = $this->buildParamsToUrl($user, $accompanyingPeriod); $params = $this->buildParamsToUrl($user, $accompanyingPeriod);
return $this->redirectToRoute('chill_calendar_calendar', $params); return $this->redirectToRoute('chill_calendar_calendar_list', $params);
} elseif ($form->isSubmitted() and !$form->isValid()) { } elseif ($form->isSubmitted() and !$form->isValid()) {
$this->addFlash('error', $this->get('translator')->trans('This form contains errors')); $this->addFlash('error', $this->get('translator')->trans('This form contains errors'));
} }
@ -259,6 +258,7 @@ class CalendarController extends AbstractController
'form' => $form->createView(), 'form' => $form->createView(),
'delete_form' => $deleteForm->createView(), 'delete_form' => $deleteForm->createView(),
'accompanyingCourse' => $accompanyingPeriod, 'accompanyingCourse' => $accompanyingPeriod,
'user' => $user,
'entity_json' => $entity_array 'entity_json' => $entity_array
]); ]);
} }
@ -274,11 +274,11 @@ class CalendarController extends AbstractController
[$user, $accompanyingPeriod] = $this->getEntity($request); [$user, $accompanyingPeriod] = $this->getEntity($request);
if ($accompanyingPeriod instanceof AccompanyingPeriod) { if ($accompanyingPeriod instanceof AccompanyingPeriod) {
$view = 'ChillCalendarBundle:Calendar:confirm_deleteAccompanyingCourse.html.twig'; $view = '@ChillCalendar/Calendar/confirm_deleteByAccompanyingCourse.html.twig';
} }
// elseif ($person instanceof Person) { elseif ($user instanceof User) {
// $view = 'ChillCalendarBundle:Calendar:confirm_deletePerson.html.twig'; $view = '@ChillCalendar/Calendar/confirm_deleteByUser.html.twig';
// } }
/* @var $entity Calendar */ /* @var $entity Calendar */
$entity = $em->getRepository('ChillCalendarBundle:Calendar')->find($id); $entity = $em->getRepository('ChillCalendarBundle:Calendar')->find($id);
@ -306,7 +306,7 @@ class CalendarController extends AbstractController
->trans("The calendar item has been successfully removed.")); ->trans("The calendar item has been successfully removed."));
$params = $this->buildParamsToUrl($user, $accompanyingPeriod); $params = $this->buildParamsToUrl($user, $accompanyingPeriod);
return $this->redirectToRoute('chill_calendar_calendar', $params); return $this->redirectToRoute('chill_calendar_calendar_list', $params);
} }
} }
@ -324,9 +324,9 @@ class CalendarController extends AbstractController
/** /**
* Creates a form to delete a Calendar entity by id. * Creates a form to delete a Calendar entity by id.
*/ */
private function createDeleteForm(int $id, ?Person $person, ?AccompanyingPeriod $accompanyingPeriod): Form private function createDeleteForm(int $id, ?User $user, ?AccompanyingPeriod $accompanyingPeriod): Form
{ {
$params = $this->buildParamsToUrl($person, $accompanyingPeriod); $params = $this->buildParamsToUrl($user, $accompanyingPeriod);
$params['id'] = $id; $params['id'] = $id;
return $this->createFormBuilder() return $this->createFormBuilder()
@ -350,7 +350,8 @@ class CalendarController extends AbstractController
throw $this->createNotFoundException('User not found'); throw $this->createNotFoundException('User not found');
} }
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $user); // TODO Add permission
// $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $user);
} elseif ($request->query->has('accompanying_period_id')) { } elseif ($request->query->has('accompanying_period_id')) {
$accompanying_period_id = $request->get('accompanying_period_id'); $accompanying_period_id = $request->get('accompanying_period_id');
$accompanyingPeriod = $em->getRepository(AccompanyingPeriod::class)->find($accompanying_period_id); $accompanyingPeriod = $em->getRepository(AccompanyingPeriod::class)->find($accompanying_period_id);

View File

@ -18,16 +18,19 @@ class CalendarRangeAPIController extends ApiController
*/ */
public function availableRanges(Request $request, string $_format): JsonResponse public function availableRanges(Request $request, string $_format): JsonResponse
{ {
if ($request->query->has('user')) {
$user = $request->query->get('user');
}
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
$query = $em->createQuery( $sql = 'SELECT c FROM ChillCalendarBundle:CalendarRange c
'SELECT c FROM ChillCalendarBundle:CalendarRange c WHERE NOT EXISTS (SELECT cal.id FROM ChillCalendarBundle:Calendar cal WHERE cal.calendarRange = c.id)';
WHERE NOT EXISTS (SELECT cal.id FROM ChillCalendarBundle:Calendar cal WHERE cal.calendarRange = c.id)')
; if ($request->query->has('user')) {
$user = $request->query->get('user');
$sql = $sql . ' AND c.user = :user';
$query = $em->createQuery($sql)
->setParameter('user', $user);
} else {
$query = $em->createQuery($sql);
}
$results = $query->getResult(); $results = $query->getResult();

View File

@ -66,13 +66,16 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
'_index' => [ '_index' => [
'methods' => [ 'methods' => [
Request::METHOD_GET => true, Request::METHOD_GET => true,
Request::METHOD_HEAD => true Request::METHOD_HEAD => true,
], ],
], ],
'_entity' => [ '_entity' => [
'methods' => [ 'methods' => [
Request::METHOD_GET => true, Request::METHOD_GET => true,
Request::METHOD_HEAD => true Request::METHOD_HEAD => true,
Request::METHOD_POST => true,
Request::METHOD_PATCH => true,
Request::METHOD_DELETE => true,
] ]
], ],
] ]

View File

@ -37,14 +37,16 @@ class Calendar
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(type="integer") * @ORM\Column(type="integer")
* @Serializer\Groups({"calendar:read"})
*/ */
private ?int $id; private ?int $id;
/** /**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @Groups({"read"}) * @Groups({"read"})
* @Serializer\Groups({"calendar:read"})
*/ */
private User $user; private ?User $user = null;
/** /**
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod") * @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod")
@ -64,6 +66,7 @@ class Calendar
* cascade={"persist", "remove", "merge", "detach"}) * cascade={"persist", "remove", "merge", "detach"})
* @ORM\JoinTable(name="chill_calendar.calendar_to_persons") * @ORM\JoinTable(name="chill_calendar.calendar_to_persons")
* @Groups({"read"}) * @Groups({"read"})
* @Serializer\Groups({"calendar:read"})
*/ */
private Collection $persons; private Collection $persons;
@ -74,6 +77,7 @@ class Calendar
* cascade={"persist", "remove", "merge", "detach"}) * cascade={"persist", "remove", "merge", "detach"})
* @ORM\JoinTable(name="chill_calendar.calendar_to_thirdparties") * @ORM\JoinTable(name="chill_calendar.calendar_to_thirdparties")
* @Groups({"read"}) * @Groups({"read"})
* @Serializer\Groups({"calendar:read"})
*/ */
private Collection $professionals; private Collection $professionals;
@ -89,6 +93,7 @@ class Calendar
/** /**
* @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_") * @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_")
* @Serializer\Groups({"calendar:read"})
*/ */
private CommentEmbeddable $comment; private CommentEmbeddable $comment;
@ -96,20 +101,20 @@ class Calendar
* @ORM\Column(type="datetimetz_immutable") * @ORM\Column(type="datetimetz_immutable")
* @Serializer\Groups({"calendar:read"}) * @Serializer\Groups({"calendar:read"})
*/ */
private \DateTimeImmutable $startDate; private ?\DateTimeImmutable $startDate = null;
/** /**
* @ORM\Column(type="datetimetz_immutable") * @ORM\Column(type="datetimetz_immutable")
* @Serializer\Groups({"calendar:read"}) * @Serializer\Groups({"calendar:read"})
*/ */
private \DateTimeImmutable $endDate; private ?\DateTimeImmutable $endDate = null;
//TODO Lieu //TODO Lieu
/** /**
* @ORM\Column(type="string", length=255) * @ORM\Column(type="string", length=255)
*/ */
private string $status; private ?string $status = null;
/** /**
* @ORM\ManyToOne(targetEntity="CancelReason") * @ORM\ManyToOne(targetEntity="CancelReason")
@ -124,7 +129,7 @@ class Calendar
/** /**
* @ORM\ManyToOne(targetEntity="Chill\ActivityBundle\Entity\Activity") * @ORM\ManyToOne(targetEntity="Chill\ActivityBundle\Entity\Activity")
*/ */
private Activity $activity; private ?Activity $activity = null;
/** /**
* @ORM\Column(type="boolean", nullable=true) * @ORM\Column(type="boolean", nullable=true)

View File

@ -25,21 +25,21 @@ class CalendarRange
/** /**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @Groups({"read"}) * @groups({"read", "write"})
*/ */
private User $user; private ?User $user = null;
/** /**
* @ORM\Column(type="datetimetz_immutable") * @ORM\Column(type="datetimetz_immutable")
* @groups({"read"}) * @groups({"read", "write"})
*/ */
private \DateTimeImmutable $startDate; private ?\DateTimeImmutable $startDate = null;
/** /**
* @ORM\Column(type="datetimetz_immutable") * @ORM\Column(type="datetimetz_immutable")
* @groups({"read"}) * @groups({"read", "write"})
*/ */
private \DateTimeImmutable $endDate; private ?\DateTimeImmutable $endDate = null;
/** /**
* @ORM\OneToMany(targetEntity=Calendar::class, * @ORM\OneToMany(targetEntity=Calendar::class,

View File

@ -37,7 +37,7 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
if (AccompanyingPeriod::STEP_DRAFT !== $period->getStep()) { if (AccompanyingPeriod::STEP_DRAFT !== $period->getStep()) {
$menu->addChild($this->translator->trans('Calendar'), [ $menu->addChild($this->translator->trans('Calendar'), [
'route' => 'chill_calendar_calendar', 'route' => 'chill_calendar_calendar_list',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $period->getId(), 'accompanying_period_id' => $period->getId(),
]]) ]])

View File

@ -7,4 +7,14 @@ div#calendarControls {
div#fullCalendar{ div#fullCalendar{
}
span.calendarRangeItems {
display: flex;
flex-direction: row;
justify-content: space-between;
a {
text-decoration: none;
padding: 3px;
}
} }

View File

@ -0,0 +1,468 @@
<template>
<div>
<h2 class="chill-red">{{ $t('edit_your_calendar_range') }}</h2>
<div class="form-check">
<input type="checkbox" id="myCalendar" class="form-check-input" v-model="showMyCalendarWidget" />
<label class="form-check-label" for="myCalendar">{{ $t('show_my_calendar') }}</label>
</div>
<div class="form-check">
<input type="checkbox" id="weekends" class="form-check-input" @click="toggleWeekends" />
<label class="form-check-label" for="weekends">{{ $t('show_weekends') }}</label>
</div>
<FullCalendar ref="fullCalendar" :options="calendarOptions">
<template v-slot:eventContent='arg' >
<span class='calendarRangeItems'>
<b v-if="arg.event.extendedProps.myCalendar" style="text-decoration: underline" >{{ arg.timeText }}</b>
<b v-else-if="!arg.event.extendedProps.myCalendar && arg.event.extendedProps.toDelete" style="text-decoration: line-through red" >{{ arg.timeText }}</b>
<b v-else >{{ arg.timeText }}</b>
<i>&nbsp;{{ arg.event.title }}</i>
<a v-if=!arg.event.extendedProps.myCalendar class="fa fa-fw fa-times"
@click.prevent="onClickDelete(arg.event)">
</a>
</span>
</template>
</FullCalendar>
<div>
<ul class="record_actions">
<li>
<button class="btn btn-save" :disabled="!dirty"
@click.prevent="onClickSave">
{{ $t('action.save')}}
</button>
<span v-if="flag.loading" class="loading">
<i class="fa fa-circle-o-notch fa-spin fa-fw"></i>
<span class="sr-only">{{ $t('loading') }}</span>
</span>
</li>
<li>
<button v-if="disableCopyDayButton" class="btn btn-action" disabled>
{{ $t('copy_range_to_next_day')}}
</button>
<button v-else class="btn btn-action"
@click.prevent="copyDay">
{{ $t('copy_range_from_day')}} {{this.lastNewDate.toLocaleDateString()}} {{ $t('to_the_next_day')}}
</button>
</li>
</ul>
</div>
<div>
<div v-if="newCalendarRanges.length > 0">
<h4>{{ $t('new_range_to_save') }}</h4>
<ul>
<li v-for="i in newCalendarRanges" :key="i.start">
{{ i.start.toLocaleString() }} - {{ i.end.toLocaleString() }}
</li>
</ul>
</div>
<div v-if="updateCalendarRanges.length > 0">
<h4>{{ $t('update_range_to_save') }}</h4>
<ul>
<li v-for="i in updateCalendarRanges" :key="i.start">
{{ i.start.toLocaleString() }} - {{ i.end.toLocaleString() }}
</li>
</ul>
</div>
<div v-if="deleteCalendarRanges.length > 0">
<h4>{{ $t('delete_range_to_save') }}</h4>
<ul>
<li v-for="i in deleteCalendarRanges" :key="i.start">
{{ i.start.toLocaleString() }} - {{ i.end.toLocaleString() }}
</li>
</ul>
</div>
</div>
</div>
<teleport to="body">
<modal v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
<template v-slot:header>
<h2 class="modal-title">{{ this.renderEventDate() }}</h2>
</template>
<template v-slot:body>
<p>{{ $t('by')}} {{this.myCalendarClickedEvent.user.username }}</p>
<p>{{ $t('main_user_concerned') }} : {{ this.myCalendarClickedEvent.mainUser.username }}</p>
<p v-if="myCalendarClickedEvent.comment.length > 0" >{{ this.myCalendarClickedEvent.comment }}</p>
</template>
<template v-slot:footer>
<ul class="record_actions">
<li>
<a
class="btn btn-show"
:href=myCalendarEventShowLink() >
</a>
</li>
<li>
<a
class="btn btn-update"
:href=myCalendarEventUpdateLink() >
</a>
</li>
<li>
<a
class="btn btn-delete"
:href=myCalendarEventDeleteLink() >
</a>
</li>
</ul>
</template>
</modal>
</teleport>
</template>
<script>
import '@fullcalendar/core/vdom'; // solves problem with Vite
import frLocale from '@fullcalendar/core/locales/fr';
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import timeGridPlugin from '@fullcalendar/timegrid';
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import { deleteCalendarRange, fetchCalendar, fetchCalendarRangesByUser, patchCalendarRange, postCalendarRange } from '../_api/api';
import { mapState } from 'vuex';
export default {
name: "App",
components: {
FullCalendar,
Modal
},
data() {
return {
errorMsg: [],
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-m"
},
flag: {
loading: false
},
userId: window.userId,
showMyCalendar: true,
myCalendarClickedEvent: null,
calendarEvents: {
userCalendar: null,
userCalendarRange: null,
new: {
events: [],
color: "#3788d8"
}
},
lastNewDate: null,
disableCopyDayButton: true,
calendarOptions: {
locale: frLocale,
plugins: [ dayGridPlugin, interactionPlugin, timeGridPlugin ],
initialView: 'timeGridWeek',
initialDate: window.startDate !== undefined ? window.startDate : new Date(),
eventSource: [],
selectable: true,
select: this.onDateSelect,
eventChange: this.onEventChange,
eventDrop: this.onEventDropOrResize,
eventResize: this.onEventDropOrResize,
eventClick: this.onEventClick,
selectMirror: false,
editable: true,
weekends: false,
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
},
}
},
computed: {
...mapState({
newCalendarRanges: state => state.newCalendarRanges,
updateCalendarRanges: state => state.updateCalendarRanges,
deleteCalendarRanges: state => state.deleteCalendarRanges,
dirty: state => state.newCalendarRanges.length > 0 || state.updateCalendarRanges.length > 0 || state.deleteCalendarRanges.length > 0
}),
showMyCalendarWidget: {
set(value) {
this.toggleMyCalendar(value);
this.updateEventsSource();
},
get() {
return this.showMyCalendar;
}
},
},
methods: {
init() {
this.fetchData();
},
openModal() {
this.modal.showModal = true;
},
myCalendarEventShowLink() {
return `/fr/calendar/calendar/${this.myCalendarClickedEvent.id}/show?user_id=${ this.userId }`
},
myCalendarEventUpdateLink() {
return `/fr/calendar/calendar/${this.myCalendarClickedEvent.id}/edit?user_id=${ this.userId }`
},
myCalendarEventDeleteLink() {
return `/fr/calendar/calendar/${this.myCalendarClickedEvent.id}/delete?user_id=${ this.userId }`
},
resetCalendar() {
this.fetchData();
this.calendarEvents.new = {
events: [],
color: "#3788d8"
};
this.updateEventsSource();
},
fetchData() {
this.flag.loading = true;
fetchCalendarRangesByUser(this.userId).then(calendarRanges => new Promise((resolve, reject) => {
let events = calendarRanges.results.map(i =>
({
start: i.startDate.datetime,
end: i.endDate.datetime,
calendarRangeId: i.id,
toDelete: false
})
);
let calendarRangeEvents = {
events: events,
borderColor: "#3788d8",
backgroundColor: '#ffffff',
textColor: '#444444',
};
this.calendarEvents.userCalendarRange = calendarRangeEvents;
fetchCalendar(this.userId).then(calendar => new Promise((resolve, reject) => {
let events = calendar.results.map(i =>
({
myCalendar: true,
calendarId: i.id,
start: i.startDate.datetime,
end: i.endDate.datetime,
user: i.user,
mainUser: i.mainUser,
persons: i.persons,
professionals: i.professionals,
comment: i.comment
})
);
let calendarEventsCurrentUser = {
events: events,
color: 'darkblue',
id: 1000,
editable: false
};
this.calendarEvents.userCalendar = calendarEventsCurrentUser;
this.updateEventsSource();
this.flag.loading = false;
resolve();
}));
resolve();
}));
},
updateEventsSource() {
this.calendarOptions.eventSources = [];
this.calendarOptions.eventSources.push(this.calendarEvents.new);
this.calendarOptions.eventSources.push(this.calendarEvents.userCalendarRange);
if (this.showMyCalendar) {
this.calendarOptions.eventSources.push(this.calendarEvents.userCalendar);
}
console.log(this.calendarOptions.eventSources);
},
toggleMyCalendar(value) {
this.showMyCalendar = value;
},
toggleWeekends: function() {
this.calendarOptions.weekends = !this.calendarOptions.weekends;
},
onDateSelect(payload) {
let events = this.calendarEvents.new.events;
events.push({
start: payload.startStr,
end: payload.endStr
});
this.calendarEvents.new = {
events: events,
borderColor: "#3788d8",
backgroundColor: '#fffadf ',
textColor: '#444444',
};
this.disableCopyDayButton = false;
this.lastNewDate = new Date(payload.startStr);
this.updateEventsSource();
this.$store.dispatch('createRange', payload);
},
onEventChange(payload) {
},
onEventDropOrResize(payload) {
payload.event.setProp('borderColor', '#3788d8');
payload.event.setProp('backgroundColor', '#fffadf');
payload.event.setProp('textColor', '#444444');
this.$store.dispatch('updateRange', payload);
},
onEventClick(payload) {
if (payload.event.extendedProps.myCalendar) {
this.myCalendarClickedEvent = {
id: payload.event.extendedProps.calendarId,
start: payload.event.start,
end: payload.event.end,
user: payload.event.extendedProps.user,
mainUser: payload.event.extendedProps.mainUser,
persons: payload.event.extendedProps.persons,
professionals: payload.event.extendedProps.professionals,
comment: payload.event.extendedProps.comment
};
console.log(this.myCalendarClickedEvent)
this.openModal();
}
},
onClickSave(payload) {
this.flag.loading = true;
if (this.$store.state.newCalendarRanges.length > 0){
Promise.all(this.$store.state.newCalendarRanges.map(cr => {
postCalendarRange({
user: {
type: 'user',
id: window.userId,
},
startDate: {
datetime: `${cr.start.toISOString().split('.')[0]}+0000`, //should be like "2021-08-20T15:00:00+0200",
},
endDate: {
datetime: `${cr.end.toISOString().split('.')[0]}+0000`, // TODO check if OK with time zone
},
})
})
).then((_r) => this.resetCalendar());
this.$store.dispatch('clearNewCalendarRanges', payload);
}
if (this.$store.state.updateCalendarRanges.length > 0){
Promise.all(this.$store.state.updateCalendarRanges.map(cr => {
patchCalendarRange(cr.id,
{
startDate: {
datetime: `${cr.start.toISOString().split('.')[0]}+0000`, //should be like "2021-08-20T15:00:00+0200",
},
endDate: {
datetime: `${cr.end.toISOString().split('.')[0]}+0000`, // TODO check if OK with time zone
},
})
})
).then((_r) => this.resetCalendar());
this.$store.dispatch('clearUpdateCalendarRanges', payload);
}
if (this.$store.state.deleteCalendarRanges.length > 0){
Promise.all(this.$store.state.deleteCalendarRanges.map(cr => {
deleteCalendarRange(cr.id)
})
).then((_r) => this.resetCalendar());
this.$store.dispatch('clearDeleteCalendarRanges', payload);
}
},
onClickDelete(payload) {
if (payload.extendedProps.hasOwnProperty("calendarRangeId")) {
if (payload.extendedProps.toDelete) {
payload.setExtendedProp('toDelete', false)
payload.setProp('borderColor', '#79bafc');
this.$store.dispatch('removeFromDeleteRange', payload);
} else {
payload.setExtendedProp('toDelete', true)
payload.setProp('borderColor', '#dddddd');
this.$store.dispatch('deleteRange', payload);
}
} else {
let newEvents = this.calendarEvents.new.events;
let filterEvents = newEvents.filter((e) =>
e.start !== payload.startStr && e.end !== payload.endStr
);
this.calendarEvents.new = {
events: filterEvents,
color: "#3788d8"
};
this.$store.dispatch('removeNewCalendarRanges', payload);
this.updateEventsSource();
}
},
isSameDay(date1, date2) {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
},
isFriday(date) {
return date.getDay() === 5
},
copyDay(_payload) {
console.log(this.calendarEvents.new);
if (this.calendarEvents.new.events.length > 0) {
// Create the copied events
let increment = !this.calendarOptions.weekends && this.isFriday(this.lastNewDate) ? 24*60*60*1000*3 : 24*60*60*1000;
let events = this.calendarEvents.new.events.filter(
i => this.isSameDay(new Date(i.start), this.lastNewDate)).map(
i => {
let startDate = new Date(new Date(i.start).getTime() + increment);
let endDate = new Date(new Date(i.end).getTime() + increment);
return ({
start: startDate.toISOString(),
end: endDate.toISOString()
})
}
);
let copiedEvents = {
events: events,
color: "#3788d8"
};
console.log(copiedEvents);
// Add to the calendar
let newEvents = this.calendarEvents.new.events;
newEvents.push(...copiedEvents.events);
this.calendarEvents.new = {
events: newEvents,
color: "#3788d8"
};
this.updateEventsSource();
// Set the last new date
this.lastNewDate = new Date(copiedEvents.events[copiedEvents.events.length - 1].start);
// Dispatch in store for saving
for (let i = 0; i < copiedEvents.events.length; i++) {
let eventObj = {
start: new Date(copiedEvents.events[i].start),
end: new Date(copiedEvents.events[i].end)
}
this.$store.dispatch('createRange', eventObj);
}
} else {
console.log('no new events to copy-paste!')
}
},
renderEventDate() {
let start = this.myCalendarClickedEvent.start;
let end = this.myCalendarClickedEvent.end;
return start.getDate() === end.getDate() ?
`${start.toLocaleDateString()}, ${start.toLocaleTimeString()} - ${end.toLocaleTimeString()}` :
`${start.toLocaleString()} - ${end.toLocaleString()}`;
}
},
mounted() {
this.init();
}
}
</script>

View File

@ -0,0 +1,21 @@
const appMessages = {
fr: {
edit_your_calendar_range: "Planifiez vos plages de disponibilités",
show_my_calendar: "Afficher mon calendrier",
show_weekends: "Afficher les week-ends",
copy_range_to_next_day: "Copier les plages du jour au jour suivant",
copy_range_from_day: "Copier les plages du ",
to_the_next_day: " au jour suivant",
copy_range_to_next_week: "Copier les plages de la semaine à la semaine suivante",
copy_range_how_to: "Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.",
new_range_to_save: "Nouvelles plages à enregistrer",
update_range_to_save: "Plages à modifier",
delete_range_to_save: "Plages à supprimer",
by: "Par",
main_user_concerned: "Utilisateur concerné"
}
}
export {
appMessages
};

View File

@ -0,0 +1,16 @@
import { createApp } from 'vue';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'
import { appMessages } from './i18n'
import store from './store'
import App from './App.vue';
const i18n = _createI18n(appMessages);
const app = createApp({
template: `<app></app>`,
})
.use(store)
.use(i18n)
.component('app', App)
.mount('#myCalendar');

View File

@ -0,0 +1,89 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { postCalendarRange, patchCalendarRange, deleteCalendarRange } from '../_api/api';
const debug = process.env.NODE_ENV !== 'production';
const store = createStore({
strict: debug,
state: {
newCalendarRanges: [],
updateCalendarRanges: [],
deleteCalendarRanges: []
},
mutations: {
updateRange(state, payload) {
state.updateCalendarRanges.push({
id: payload.event.extendedProps.calendarRangeId,
start: payload.event.start,
end: payload.event.end
});
},
addRange(state, payload) {
state.newCalendarRanges.push({
start: payload.start,
end: payload.end
});
},
deleteRange(state, payload) {
state.deleteCalendarRanges.push({
id: payload.extendedProps.calendarRangeId,
start: payload.start,
end: payload.end
});
},
clearNewCalendarRanges(state) {
state.newCalendarRanges = [];
},
clearUpdateCalendarRanges(state) {
state.updateCalendarRanges = [];
},
clearDeleteCalendarRanges(state) {
state.deleteCalendarRanges = [];
},
removeNewCalendarRanges(state, payload) {
let filteredCollection = state.newCalendarRanges.filter(
(e) => e.start.toString() !== payload.start.toString() && e.end.toString() !== payload.end.toString()
)
state.newCalendarRanges = filteredCollection;
},
removeFromDeleteRange(state, payload) {
let filteredCollection = state.deleteCalendarRanges.filter(
(e) => e.start.toString() !== payload.start.toString() && e.end.toString() !== payload.end.toString()
)
state.deleteCalendarRanges = filteredCollection;
},
},
actions: {
createRange({ commit }, payload) {
console.log('### action createRange', payload);
commit('addRange', payload);
},
updateRange({ commit }, payload) {
console.log('### action updateRange', payload);
commit('updateRange', payload);
},
deleteRange({ commit }, payload) {
console.log('### action deleteRange', payload);
commit('deleteRange', payload);
},
clearNewCalendarRanges({ commit }, payload) {
commit('clearNewCalendarRanges', payload);
},
clearUpdateCalendarRanges({ commit }, payload) {
commit('clearUpdateCalendarRanges', payload);
},
clearDeleteCalendarRanges({ commit }, payload) {
commit('clearDeleteCalendarRanges', payload);
},
removeNewCalendarRanges({ commit }, payload) {
commit('removeNewCalendarRanges', payload);
},
removeFromDeleteRange({ commit }, payload) {
commit('removeFromDeleteRange', payload);
},
}
});
export default store;

View File

@ -0,0 +1,100 @@
/*
* Endpoint chill_api_single_calendar_range
* method GET, get Calendar ranges
* @returns {Promise} a promise containing all Calendar ranges objects
*/
const fetchCalendarRanges = () => {
const url = `/api/1.0/calendar/calendar-range-available.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
const fetchCalendarRangesByUser = (userId) => {
const url = `/api/1.0/calendar/calendar-range-available.json?user=${userId}`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_calendar
* method GET, get Calendar events, can be filtered by mainUser
* @returns {Promise} a promise containing all Calendar objects
*/
const fetchCalendar = (mainUserId) => {
const url = `/api/1.0/calendar/calendar.json?main_user=${mainUserId}&item_per_page=1000`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_calendar_range__entity_create
* method POST, post CalendarRange entity
*/
const postCalendarRange = (body) => {
const url = `/api/1.0/calendar/calendar-range.json?`;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
}).then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_calendar_range__entity
* method PATCH, patch CalendarRange entity
*/
const patchCalendarRange = (id, body) => {
console.log(body)
const url = `/api/1.0/calendar/calendar-range/${id}.json`;
return fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
}).then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_calendar_range__entity
* method DELETE, delete CalendarRange entity
*/
const deleteCalendarRange = (id) => {
const url = `/api/1.0/calendar/calendar-range/${id}.json`;
return fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
}).then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export {
fetchCalendarRanges,
fetchCalendar,
fetchCalendarRangesByUser,
postCalendarRange,
patchCalendarRange,
deleteCalendarRange
};

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="calendar__controls"> <div>
<h2 class="chill-red">{{ $t('choose_your_calendar_user') }}</h2> <h2 class="chill-red">{{ $t('choose_your_calendar_user') }}</h2>
<VueMultiselect <VueMultiselect
name="field" name="field"
@ -31,7 +31,7 @@
</template> </template>
<script> <script>
import { fetchCalendarRanges, fetchCalendar } from './js/api' import { fetchCalendarRanges, fetchCalendar } from '../../_api/api'
import VueMultiselect from 'vue-multiselect'; import VueMultiselect from 'vue-multiselect';
import { whoami } from 'ChillPersonAssets/vuejs/AccompanyingCourse/api'; import { whoami } from 'ChillPersonAssets/vuejs/AccompanyingCourse/api';
@ -201,12 +201,3 @@ export default {
<style src="vue-multiselect/dist/vue-multiselect.css"></style> <style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss" scoped>
div.calendar__controls {
background-color: 'black';
height: 50%;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
</style>

View File

@ -1,32 +0,0 @@
/*
* Endpoint chill_api_single_calendar_range
* method GET, get Calendar ranges
* @returns {Promise} a promise containing all Calendar ranges objects
*/
const fetchCalendarRanges = () => {
const url = `/api/1.0/calendar/calendar-range-available.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_calendar
* method GET, get Calendar events, can be filtered by mainUser
* @returns {Promise} a promise containing all Calendar objects
*/
const fetchCalendar = (mainUserId) => {
const url = `/api/1.0/calendar/calendar.json?main_user=${mainUserId}&item_per_page=1000`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export {
fetchCalendarRanges,
fetchCalendar
};

View File

@ -1,6 +1,6 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} {% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar' %} {% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title 'Remove calendar item'|trans %} {% block title 'Remove calendar item'|trans %}
@ -9,8 +9,8 @@
{ {
'title' : 'Remove calendar item'|trans, 'title' : 'Remove calendar item'|trans,
'confirm_question' : 'Are you sure you want to remove the calendar item?'|trans, 'confirm_question' : 'Are you sure you want to remove the calendar item?'|trans,
'cancel_route' : 'chill_calendar_calendar', 'cancel_route' : 'chill_calendar_calendar_list',
'cancel_parameters' : { 'accompanying_course_id' : accompanyingCourse.id, 'id' : calendar.id }, 'cancel_parameters' : { 'accompanying_period_id' : accompanyingCourse.id, 'id' : calendar.id },
'form' : delete_form 'form' : delete_form
} ) }} } ) }}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "@ChillMain/layout.html.twig" %}
{% set user = calendar.user %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title 'Remove activity'|trans %}
{% block content %}
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'Remove calendar item'|trans,
'confirm_question' : 'Are you sure you want to remove the calendar item?'|trans,
'cancel_route' : 'chill_calendar_calendar_list',
'cancel_parameters' : { 'user_id' : calendar.user.id, 'id' : calendar.id },
'form' : delete_form
} ) }}
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = 'chill_activity_activity_list' %}
{% set person = activity.person %}
{% block title 'Remove activity'|trans %}
{% block personcontent %}
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'Remove activity'|trans,
'confirm_question' : 'Are you sure you want to remove the activity about "%name%" ?'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname } ),
'cancel_route' : 'chill_activity_activity_list',
'cancel_parameters' : { 'person_id' : activity.person.id, 'id' : activity.id },
'form' : delete_form
} ) }}
{% endblock %}

View File

@ -48,16 +48,20 @@
{{ form_row(form.sendSMS) }} {{ form_row(form.sendSMS) }}
{% endif %} {% endif %}
{% if context == 'user' %}
<div id="calendarControls"></div>
{% endif %}
<div id="fullCalendar"></div> <div id="fullCalendar"></div>
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">
<a <a
class="btn btn-cancel" class="btn btn-cancel"
{%- if context == 'person' -%} {%- if context == 'user' -%}
href="{{ chill_return_path_or('chill_calendar_calendar', { 'person_id': person.id } )}}" href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'user_id': user.id } )}}"
{%- else -%} {%- elseif context == 'accompanyingCourse' -%}
href="{{ chill_return_path_or('chill_calendar_calendar', { 'accompanying_period_id': accompanyingCourse.id } )}}" href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'accompanying_period_id': accompanyingCourse.id } )}}"
{%- endif -%} {%- endif -%}
> >
{{ 'Cancel'|trans|chill_return_path_label }} {{ 'Cancel'|trans|chill_return_path_label }}

View File

@ -2,7 +2,7 @@
{% set activeRouteKey = 'chill_calendar_calendar_list' %} {% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title 'Update calendar'|trans %} {% block title 'Update calendar'|trans %}
{% block content %} {% block content %}
<div class="calendar-edit"> <div class="calendar-edit">
@ -35,6 +35,7 @@
{% block css %} {% block css %}
{{ parent() }} {{ parent() }}
{{ encore_entry_link_tags('vue_calendar') }} {{ encore_entry_link_tags('vue_calendar') }}
{{ encore_entry_link_tags('page_calendar') }}
{% endblock %} {% endblock %}
{% block block_post_menu %} {% block block_post_menu %}

View File

@ -0,0 +1,36 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title 'Update calendar'|trans %}
{% block content %}
<div class="calendar-edit">
<div class="row justify-content-center">
<div class="col-md-10 col-xxl">
<div id="calendar"></div> {# <=== vue component #}
{% include 'ChillCalendarBundle:Calendar:edit.html.twig' with {'context': 'user'} %}
</div>
</div>
</div>
{% endblock %}
{% block js %}
{{ parent() }}
<script type="text/javascript">
window.addEventListener('DOMContentLoaded', function (e) {
chill.displayAlertWhenLeavingModifiedForm('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.entity = {{ entity_json|json_encode|raw }};
window.startDate = {{ entity.startDate|date('Y-m-d H:i:s')|json_encode|raw }};
window.endDate = {{ entity.endDate|date('Y-m-d H:i:s')|json_encode|raw }};
window.mainUser = {{ entity.mainUser.id }};
</script>
{{ encore_entry_script_tags('vue_calendar') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('vue_calendar') }}
{% endblock %}

View File

@ -8,134 +8,128 @@
{% set accompanying_course_id = accompanyingCourse.id %} {% set accompanying_course_id = accompanyingCourse.id %}
{% endif %} {% endif %}
<h2>{{ 'Calendar list' |trans }}</h2> {% if context == 'user' %}
<h2>{{ 'My calendar list' |trans }}</h2>
{% else %}
<h2>{{ 'Calendar list' |trans }}</h2>
{% endif %}
{% if calendarItems|length == 0 %} {% if context == 'user' %}
<p class="chill-no-data-statement"> <div id="myCalendar"></div>
{{ "There is no calendar items."|trans }}
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-create button-small"></a>
</p>
{% else %} {% else %}
<div class="flex-table list-records context-{{ context }}"> {% if calendarItems|length == 0 %}
<p class="chill-no-data-statement">
{{ "There is no calendar items."|trans }}
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-create button-small"></a>
</p>
{% else %}
{% for calendar in calendarItems %} <div class="flex-table list-records context-{{ context }}">
<div class="item-bloc"> {% for calendar in calendarItems %}
<div class="item-row main">
<div class="item-col"> <div class="item-bloc">
<div class="item-row main">
<div class="item-col">
{% if calendar.startDate and calendar.endDate %} {% if calendar.startDate and calendar.endDate %}
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %} {% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
<h3>{{ "From the day"|trans }} {{ calendar.startDate|format_datetime('medium', 'short') }} </h3> <h3>{{ "From the day"|trans }} {{ calendar.startDate|format_datetime('medium', 'short') }} </h3>
<h3>{{ "to the day"|trans }} {{ calendar.endDate|format_datetime('medium', 'short') }}</h3> <h3>{{ "to the day"|trans }} {{ calendar.endDate|format_datetime('medium', 'short') }}</h3>
{% else %} {% else %}
<h3>{{ calendar.startDate|format_date('full') }} </h3> <h3>{{ calendar.startDate|format_date('full') }} </h3>
<h3>{{ calendar.startDate|format_datetime('none', 'short', locale='fr') }} - {{ calendar.endDate|format_datetime('none', 'short', locale='fr') }}</h3> <h3>{{ calendar.startDate|format_datetime('none', 'short', locale='fr') }} - {{ calendar.endDate|format_datetime('none', 'short', locale='fr') }}</h3>
<div class="duration">
<p>
<i class="fa fa-fw fa-hourglass-end"></i>
{{ calendar.endDate.diff(calendar.startDate)|date("%H:%M")}}
</p>
</div>
{% endif %}
<div class="duration">
<p>
<i class="fa fa-fw fa-hourglass-end"></i>
{{ calendar.endDate.diff(calendar.startDate)|date("%H:%M")}}
</p>
</div>
{% endif %} {% endif %}
</div>
<div class="item-col">
<ul class="list-content">
{% if calendar.user %}
<li>
<b>{{ 'by'|trans }}{{ calendar.user.usernameCanonical }}</b>
</li>
{% endif %}
{% if calendar.mainUser is not empty %}
<li>
<b>{{ 'main user concerned'|trans }}: {{ calendar.mainUser.usernameCanonical }}</b>
</li>
{% endif %}
</ul>
<ul class="record_actions">
<li>
<a href="{{ path('chill_calendar_calendar_show', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-show "></a>
</li>
{# TOOD
{% if is_granted('CHILL_ACTIVITY_UPDATE', calendar) %}
#}
<li>
<a href="{{ path('chill_calendar_calendar_edit', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-update "></a>
</li>
{# TOOD
{% endif %}
{% if is_granted('CHILL_ACTIVITY_DELETE', calendar) %}
#}
<li>
<a href="{{ path('chill_calendar_calendar_delete', { 'id': calendar.id, 'user_id' : user_id, 'accompanying_period_id': accompanying_course_id } ) }}" class="btn btn-delete "></a>
</li>
{#
{% endif %}
#}
</ul>
</div>
</div>
{%
if calendar.comment.comment is not empty
or calendar.users|length > 0
or calendar.thirdParties|length > 0
or calendar.users|length > 0
%}
<div class="item-row details">
<div class="item-col">
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'with_display': 'row', 'entity': calendar } %}
</div>
{% if calendar.comment.comment is not empty %}
<div class="item-col comment">
{{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
</div>
{% endif %} {% endif %}
{% if context == 'user' and calendar.accompanyingPeriod is not empty %}
<a class="btn btn-sm btn-outline-primary"
title="{{ 'Period number %number%'|trans({'%number%': calendar.accompanyingPeriod.id}) }}"
href="{{ chill_path_add_return_path(
"chill_user_accompanying_course_index",
{ 'accompanying_period_id': calendar.accompanyingPeriod.id }
) }}"><i class="fa fa-random"></i>
</a>
{% endif %}
</div>
<div class="item-col">
<ul class="list-content">
{% if calendar.user %}
<li>
<b>{{ 'by'|trans }}{{ calendar.user.usernameCanonical }}</b>
</li>
{% endif %}
{% if calendar.mainUser is not empty %}
<li>
<b>{{ 'main user concerned'|trans }}: {{ calendar.mainUser.usernameCanonical }}</b>
</li>
{% endif %}
<li>
{%- if calendar.comment.isEmpty -%}
<span class="chill-no-data-statement">{{ 'No comments'|trans }}</span>
{%- else -%}
{{ calendar.comment|chill_entity_render_box }}
{%- endif -%}
</li>
</ul>
<ul class="record_actions">
<li>
<a href="{{ path('chill_calendar_calendar_show', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-show "></a>
</li>
{# TOOD
{% if is_granted('CHILL_ACTIVITY_UPDATE', calendar) %}
#}
<li>
<a href="{{ path('chill_calendar_calendar_edit', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-update "></a>
</li>
{# TOOD
{% endif %}
{% if is_granted('CHILL_ACTIVITY_DELETE', calendar) %}
#}
<li>
<a href="{{ path('chill_calendar_calendar_delete', { 'id': calendar.id, 'user_id' : user_id, 'accompanying_period_id': accompanying_course_id } ) }}" class="btn btn-delete "></a>
</li>
{#
{% endif %}
#}
</ul>
</div>
</div>
{%
if calendar.comment.comment is not empty
or calendar.users|length > 0
or calendar.thirdParties|length > 0
or calendar.users|length > 0
%}
<div class="item-row details">
<div class="item-col">
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'with_display': 'row', 'entity': calendar } %}
</div>
{% if calendar.comment.comment is not empty %}
<div class="item-col comment">
{{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endfor %}
</div>
{% endif %}
{% if context != 'user' %}
{# TODO set this condition in configuration #}
<ul class="record_actions">
<li>
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-create">
{{ 'Add a new calendar' | trans }}
</a>
</li>
</ul>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %} {% endif %}
{% if context != 'user' %}
{# TODO set this condition in configuration #}
<ul class="record_actions">
<li>
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-create">
{{ 'Add a new calendar' | trans }}
</a>
</li>
</ul>
{% endif %}

View File

@ -0,0 +1,23 @@
{% extends "@ChillMain/layout.html.twig" %}
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
{% block title %}{{ 'My calendar list' |trans }}{% endblock title %}
{% block content %}
{% include 'ChillCalendarBundle:Calendar:list.html.twig' with {'context': 'user'} %}
{% endblock %}
{% block js %}
{{ parent() }}
<script type="text/javascript">
window.userId = {{ user.id }};
</script>
{{ encore_entry_script_tags('vue_mycalendarrange') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('vue_calendar') }}
{{ encore_entry_link_tags('page_calendar') }}
{% endblock %}

View File

@ -51,9 +51,9 @@
<a <a
class="btn btn-cancel" class="btn btn-cancel"
{%- if context == 'person' -%} {%- if context == 'person' -%}
href="{{ chill_return_path_or('chill_calendar_calendar', { 'person_id': person.id } )}}" href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'person_id': person.id } )}}"
{%- else -%} {%- else -%}
href="{{ chill_return_path_or('chill_calendar_calendar', { 'accompanying_period_id': accompanyingCourse.id } )}}" href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'accompanying_period_id': accompanyingCourse.id } )}}"
{%- endif -%} {%- endif -%}
> >
{{ 'Cancel'|trans|chill_return_path_label }} {{ 'Cancel'|trans|chill_return_path_label }}

View File

@ -31,7 +31,7 @@
{% block css %} {% block css %}
{{ parent() }} {{ parent() }}
<link rel="stylesheet" href="{{ asset('build/vue_calendar.css') }}"/> {{ encore_entry_link_tags('vue_calendar') }}
{% endblock %} {% endblock %}
{% block block_post_menu %} {% block block_post_menu %}

View File

@ -58,24 +58,37 @@
{% set accompanying_course_id = accompanyingCourse.id %} {% set accompanying_course_id = accompanyingCourse.id %}
{% endif %} {% endif %}
{% set user_id = null %}
{% if user %}
{% set user_id = user.id %}
{% endif %}
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">
<a class="btn btn-cancel" href="{{ path('chill_calendar_calendar', { 'accompanying_period_id': accompanying_course_id } ) }}"> <a class="btn btn-cancel" href="{{ path('chill_calendar_calendar_list', { 'accompanying_period_id': accompanying_course_id, 'user_id': user_id } ) }}">
{{ 'Back to the list'|trans }} {{ 'Back to the list'|trans }}
</a> </a>
</li> </li>
{% if accompanyingCourse %}
<li> <li>
<a class="btn btn-update" href="{{ path('chill_calendar_calendar_edit', { 'id': entity.id, 'accompanying_period_id': accompanying_course_id }) }}"> <a class="btn btn-update" href="{{ path('chill_calendar_calendar_edit', { 'id': entity.id, 'accompanying_period_id': accompanying_course_id }) }}">
{{ 'Edit'|trans }} {{ 'Edit'|trans }}
</a> </a>
</li> </li>
{% endif %}
{% if user %}
<li>
<a class="btn btn-update" href="{{ path('chill_calendar_calendar_edit', { 'id': entity.id, 'user_id': user_id }) }}">
{{ 'Edit'|trans }}
</a>
</li>
{% endif %}
{# TODO {# TODO
{% if is_granted('CHILL_ACTIVITY_DELETE', entity) %} {% if is_granted('CHILL_ACTIVITY_DELETE', entity) %}
#} #}
<li> <li>
<a href="{{ path('chill_calendar_calendar_delete', { 'id': entity.id, 'accompanying_period_id': accompanying_course_id } ) }}" class="btn btn-delete"> <a href="{{ path('chill_calendar_calendar_delete', { 'id': entity.id, 'accompanying_period_id': accompanying_course_id, 'user_id': user_id } ) }}" class="btn btn-delete">
{{ 'Delete'|trans }} {{ 'Delete'|trans }}
</a> </a>
</li> </li>

View File

@ -0,0 +1,13 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title 'Calendar'|trans %}
{% block content -%}
<div class="calendar-show">
<div class="row justify-content-center">
<div class="col-md-10 col-xxl">
{% include 'ChillCalendarBundle:Calendar:show.html.twig' with {'context': 'user'} %}
</div>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,66 @@
<?php
namespace Chill\CalendarBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
class CalendarControllerTest extends WebTestCase
{
/**
* Setup before the first test of this class (see phpunit doc)
*/
public static function setUpBeforeClass()
{
static::bootKernel();
}
/**
* Setup before each test method (see phpunit doc)
*/
public function setUp()
{
$this->client = static::createClient(array(), array(
'PHP_AUTH_USER' => 'center a_social',
'PHP_AUTH_PW' => 'password',
));
}
private function getAccompanyingPeriodFromFixtures(): AccompanyingPeriod
{
$em = static::$kernel->getContainer()
->get('doctrine.orm.entity_manager');
$accompanying_period = $em->getRepository('ChillPersonBundle:AccompanyingPeriod')->find(1);
if ($accompanying_period === NULL) {
throw new \RuntimeException("We need an accompanying course with id = 1. Did you add fixtures ?");
}
return $accompanying_period;
}
public function testList()
{
$this->client->request(
Request::METHOD_GET,
sprintf('/fr/calendar/?accompanying_period_id=%d', $this->getAccompanyingPeriodFromFixtures()->getId())
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
public function testNew()
{
$this->client->request(
Request::METHOD_GET,
sprintf('/fr/calendar/new?accompanying_period_id=%d', $this->getAccompanyingPeriodFromFixtures()->getId())
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
}

View File

@ -8,6 +8,28 @@ servers:
- url: "/api" - url: "/api"
description: "Your current dev server" description: "Your current dev server"
components:
schemas:
Date:
type: object
properties:
datetime:
type: string
format: date-time
User:
type: object
properties:
id:
type: integer
type:
type: string
enum:
- user
username:
type: string
text:
type: string
paths: paths:
/1.0/calendar/calendar.json: /1.0/calendar/calendar.json:
get: get:
@ -48,6 +70,34 @@ paths:
responses: responses:
200: 200:
description: "ok" description: "ok"
post:
tags:
- calendar
summary: create a new calendar range
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
user:
$ref: '#/components/schemas/User'
startDate:
$ref: '#/components/schemas/Date'
endDate:
$ref: '#/components/schemas/Date'
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
/1.0/calendar/calendar-range/{id}.json: /1.0/calendar/calendar-range/{id}.json:
get: get:
@ -70,6 +120,56 @@ paths:
description: "not found" description: "not found"
401: 401:
description: "Unauthorized" description: "Unauthorized"
patch:
tags:
- calendar
summary: update a calendar range
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
user:
$ref: '#/components/schemas/User'
startDate:
$ref: '#/components/schemas/Date'
endDate:
$ref: '#/components/schemas/Date'
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
delete:
tags:
- calendar
summary: "Remove a calendar range"
parameters:
- name: id
in: path
required: true
description: The calendar range id
schema:
type: integer
format: integer
minimum: 1
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "object with validation errors"
/1.0/calendar/calendar-range-available.json: /1.0/calendar/calendar-range-available.json:
get: get:

View File

@ -1,10 +1,11 @@
// this file loads all assets from the Chill calendar bundle // this file loads all assets from the Chill calendar bundle
module.exports = function(encore, entries) { module.exports = function(encore, entries) {
entries.push(__dirname + '/Resources/public/chill/index.js');
encore.addAliases({ encore.addAliases({
ChillCalendarAssets: __dirname + '/Resources/public' ChillCalendarAssets: __dirname + '/Resources/public'
}); });
encore.addEntry('vue_calendar', __dirname + '/Resources/public/vuejs/Calendar/index.js'); encore.addEntry('vue_calendar', __dirname + '/Resources/public/vuejs/Calendar/index.js');
encore.addEntry('vue_mycalendarrange', __dirname + '/Resources/public/vuejs/MyCalendarRange/index.js');
encore.addEntry('page_calendar', __dirname + '/Resources/public/chill/index.js');
}; };

View File

@ -1,5 +1,6 @@
Calendar: Rendez-vous Calendar: Rendez-vous
Calendar list: Liste des rendez-vous Calendar list: Liste des rendez-vous
My calendar list: Mes rendez-vous
There is no calendar items.: Il n'y a pas de rendez-vous There is no calendar items.: Il n'y a pas de rendez-vous
Remove calendar item: Supprimer le rendez-vous Remove calendar item: Supprimer le rendez-vous
Are you sure you want to remove the calendar item?: Êtes-vous sûr de vouloir supprimer le rendez-vous? Are you sure you want to remove the calendar item?: Êtes-vous sûr de vouloir supprimer le rendez-vous?

View File

@ -87,6 +87,8 @@ class ApiController extends AbstractCRUDController
return $this->entityPut('_entity', $request, $id, $_format); return $this->entityPut('_entity', $request, $id, $_format);
case Request::METHOD_POST: case Request::METHOD_POST:
return $this->entityPostAction('_entity', $request, $id, $_format); return $this->entityPostAction('_entity', $request, $id, $_format);
case Request::METHOD_DELETE:
return $this->entityDelete('_entity', $request, $id, $_format);
default: default:
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented"); throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented");
} }
@ -217,6 +219,54 @@ class ApiController extends AbstractCRUDController
$this->getContextForSerializationPostAlter($action, $request, $_format, $entity) $this->getContextForSerializationPostAlter($action, $request, $_format, $entity)
); );
} }
public function entityDelete($action, Request $request, $id, string $_format): Response
{
$entity = $this->getEntity($action, $id, $request, $_format);
if (NULL === $entity) {
throw $this->createNotFoundException(sprintf("The %s with id %s "
. "is not found", $this->getCrudName(), $id));
}
$response = $this->checkACL($action, $request, $_format, $entity);
if ($response instanceof Response) {
return $response;
}
$response = $this->onPostCheckACL($action, $request, $_format, $entity);
if ($response instanceof Response) {
return $response;
}
$response = $this->onBeforeSerialize($action, $request, $_format, $entity);
if ($response instanceof Response) {
return $response;
}
$errors = $this->validate($action, $request, $_format, $entity);
$response = $this->onAfterValidation($action, $request, $_format, $entity, $errors);
if ($response instanceof Response) {
return $response;
}
if ($errors->count() > 0) {
$response = $this->json($errors);
$response->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
return $response;
}
$this->getDoctrine()->getManager()->remove($entity);
$this->getDoctrine()->getManager()->flush();
$response = $this->onAfterFlush($action, $request, $_format, $entity, $errors);
if ($response instanceof Response) {
return $response;
}
return $this->json(Response::HTTP_OK);
}
protected function onAfterValidation(string $action, Request $request, string $_format, $entity, ConstraintViolationListInterface $errors, array $more = []): ?Response protected function onAfterValidation(string $action, Request $request, string $_format, $entity, ConstraintViolationListInterface $errors, array $more = []): ?Response
{ {

View File

@ -1141,7 +1141,7 @@ class CRUDController extends AbstractController
*/ */
protected function getPaginatorFactory(): PaginatorFactory protected function getPaginatorFactory(): PaginatorFactory
{ {
return $this->container->get(PaginatorFactory::class); return $this->container->get('chill_main.paginator_factory');
} }
/** /**
@ -1196,7 +1196,7 @@ class CRUDController extends AbstractController
return \array_merge( return \array_merge(
parent::getSubscribedServices(), parent::getSubscribedServices(),
[ [
PaginatorFactory::class => PaginatorFactory::class, 'chill_main.paginator_factory' => PaginatorFactory::class,
'translator' => TranslatorInterface::class, 'translator' => TranslatorInterface::class,
AuthorizationHelper::class => AuthorizationHelper::class, AuthorizationHelper::class => AuthorizationHelper::class,
EventDispatcherInterface::class => EventDispatcherInterface::class, EventDispatcherInterface::class => EventDispatcherInterface::class,

View File

@ -2,6 +2,7 @@
namespace Chill\MainBundle; namespace Chill\MainBundle;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
@ -23,6 +24,10 @@ class ChillMainBundle extends Bundle
public function build(ContainerBuilder $container) public function build(ContainerBuilder $container)
{ {
parent::build($container); parent::build($container);
$container->registerForAutoconfiguration(LocalMenuBuilderInterface::class)
->addTag('chill.menu_builder');
$container->addCompilerPass(new SearchableServicesCompilerPass()); $container->addCompilerPass(new SearchableServicesCompilerPass());
$container->addCompilerPass(new ConfigConsistencyCompilerPass()); $container->addCompilerPass(new ConfigConsistencyCompilerPass());
$container->addCompilerPass(new TimelineCompilerClass()); $container->addCompilerPass(new TimelineCompilerClass());

View File

@ -15,7 +15,7 @@ use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;
/** /**
* Load fixtures users into database * Load fixtures users into database
* *
* create a user for each permission_group and center. * create a user for each permission_group and center.
* username and password are identicals. * username and password are identicals.
* *
@ -28,12 +28,12 @@ class LoadUsers extends AbstractFixture implements OrderedFixtureInterface, Cont
* @var ContainerInterface * @var ContainerInterface
*/ */
private $container; private $container;
public function getOrder() public function getOrder()
{ {
return 1000; return 1000;
} }
public static $refs = array( public static $refs = array(
'center a_social' => array( 'center a_social' => array(
'groupCenterRefs' => ['centerA_permission_group_social'] 'groupCenterRefs' => ['centerA_permission_group_social']
@ -54,10 +54,10 @@ class LoadUsers extends AbstractFixture implements OrderedFixtureInterface, Cont
'groupCenterRefs' => ['centerB_permission_group_direction'] 'groupCenterRefs' => ['centerB_permission_group_direction']
), ),
'multi_center' => array( 'multi_center' => array(
'groupCenterRefs' => ['centerA_permission_group_social', 'groupCenterRefs' => ['centerA_permission_group_social',
'centerB_permission_group_social'] 'centerB_permission_group_social']
) )
); );
public function load(ObjectManager $manager) public function load(ObjectManager $manager)
@ -67,11 +67,11 @@ class LoadUsers extends AbstractFixture implements OrderedFixtureInterface, Cont
$user = new User(); $user = new User();
$defaultEncoder = new MessageDigestPasswordEncoder('sha512', true, 5000); $defaultEncoder = new MessageDigestPasswordEncoder('sha512', true, 5000);
$encoderFactory = new EncoderFactory([ $encoderFactory = new EncoderFactory([
User::class => $defaultEncoder User::class => $defaultEncoder
]); ]);
$user $user
->setUsername($username) ->setUsername($username)
->setPassword($encoderFactory ->setPassword($encoderFactory
@ -84,7 +84,7 @@ class LoadUsers extends AbstractFixture implements OrderedFixtureInterface, Cont
foreach ($params['groupCenterRefs'] as $groupCenterRef) { foreach ($params['groupCenterRefs'] as $groupCenterRef) {
$user->addGroupCenter($this->getReference($groupCenterRef)); $user->addGroupCenter($this->getReference($groupCenterRef));
} }
echo 'Creating user ' . $username ."... \n"; echo 'Creating user ' . $username ."... \n";
$manager->persist($user); $manager->persist($user);
$this->addReference($username, $user); $this->addReference($username, $user);
@ -98,7 +98,7 @@ class LoadUsers extends AbstractFixture implements OrderedFixtureInterface, Cont
if (NULL === $container) { if (NULL === $container) {
throw new \LogicException('$container should not be null'); throw new \LogicException('$container should not be null');
} }
$this->container = $container; $this->container = $container;
} }

View File

@ -78,6 +78,7 @@ export default {
@import 'ChillMainAssets/module/bootstrap/shared'; @import 'ChillMainAssets/module/bootstrap/shared';
@import 'ChillPersonAssets/chill/scss/mixins'; @import 'ChillPersonAssets/chill/scss/mixins';
div#accompanying-course {
span.multiselect__tag { span.multiselect__tag {
@include badge_social_issue; @include badge_social_issue;
background: $chill-l-gray; background: $chill-l-gray;
@ -93,6 +94,7 @@ export default {
} }
} }
} }
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<h2>{{ $t('household_members_editor.concerned.title') }}</h2> <h2 class="mt-4">{{ $t('household_members_editor.concerned.title') }}</h2>
<h3 v-if="needsPositionning"> <h3 v-if="needsPositionning">
{{ $t('household_members_editor.concerned.persons_to_positionnate') }} {{ $t('household_members_editor.concerned.persons_to_positionnate') }}

View File

@ -1,30 +1,71 @@
<template> <template>
<h2>{{ $t('household_members_editor.household_part') }}</h2>
<h2 class="mt-4">{{ $t('household_members_editor.household_part') }}</h2>
<div v-if="hasHousehold"> <div v-if="hasHousehold">
<div class="flex-table"> <div class="flex-table">
<div class="item-bloc"> <div class="item-bloc">
<household-render-box :household="household" :isAddressMultiline="true"></household-render-box> <household-render-box :household="household" :isAddressMultiline="true"></household-render-box>
</div> </div>
</div> </div>
<div v-if="isHouseholdNew && !hasHouseholdAddress"> <div v-if="isHouseholdNew && !hasHouseholdAddress">
<h3>{{ $t('household_members_editor.household.where_live_the_household') }}</h3> <div v-if="hasAddressSuggestion" class="householdAddressSuggestion my-5">
<h4 class="mb-3">
<div v-if="filterAddressesSuggestion.length > 0" class="flex-table householdAddressSuggestionList"> {{ $t('household_members_editor.household.where_live_the_household') }}
<div v-for="a in filterAddressesSuggestion" class="item-bloc"> </h4>
<address-render-box :address="a"></address-render-box> <div class="accordion" id="addressSuggestions">
<ul class="record_actions"> <div class="accordion-item">
<li> <h2 class="accordion-header" id="heading_address_suggestions">
<button class="btn btn-action" @click="setHouseholdAddress(a)"> <button v-if="!showAddressSuggestion"
{{ $t('household_members_editor.household.household_live_to_this_address') }} class="accordion-button collapsed"
</button> type="button"
</li> data-bs-toggle="collapse"
</ul> aria-expanded="false"
</div> @click="toggleAddressSuggestion">
</div> {{ $tc('household_members_editor.show_household_suggestion', countAddressSuggestion) }}
<div v-else> </button>
<span class="chill-no-data-statement">{{ $t('household_members_editor.household.no_suggestions') }}</span> <button v-if="showAddressSuggestion"
class="accordion-button"
type="button"
data-bs-toggle="collapse"
aria-expanded="true"
@click="toggleAddressSuggestion">
{{ $t('household_members_editor.hide_household_suggestion') }}
</button>
</h2>
<div class="accordion-collapse" id="collapse_address_suggestions"
aria-labelledby="heading_address_suggestions" data-bs-parent="#addressSuggestions">
<div v-if="showAddressSuggestion">
<div class="flex-table householdAddressSuggestionList">
<div v-for="a in filterAddressesSuggestion" class="item-bloc">
<div class="float-button bottom">
<div class="box">
<div class="action">
<ul class="record_actions">
<li>
<button class="btn btn-sm btn-choose" @click="setHouseholdAddress(a)">
{{ $t('household_members_editor.household.household_live_to_this_address') }}
</button>
</li>
</ul>
</div>
<ul class="list-content fa-ul">
<li>
<i class="fa fa-li fa-map-marker"></i>
<address-render-box :address="a"></address-render-box>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<ul class="record_actions"> <ul class="record_actions">
@ -39,8 +80,8 @@
</add-address> </add-address>
</li> </li>
</ul> </ul>
</div> </div>
<div v-if="isHouseholdNew && hasHouseholdAddress"> <div v-if="isHouseholdNew && hasHouseholdAddress">
<ul class="record_actions"> <ul class="record_actions">
<li > <li >
@ -49,34 +90,15 @@
</button> </button>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div v-else-if="isForceLeaveWithoutHousehold"> <div v-else-if="isForceLeaveWithoutHousehold">
{{ $t('household_members_editor.household.will_leave_any_household') }} {{ $t('household_members_editor.household.will_leave_any_household') }}
</div> </div>
<div v-else> <div v-else class="alert alert-info">{{ $t('household_members_editor.household.no_household_choose_one') }}</div>
<div class="alert alert-info">{{ $t('household_members_editor.household.no_household_choose_one') }}</div>
</div>
<ul v-if="allowChangeHousehold" class="record_actions"> <ul v-if="allowChangeHousehold" class="record_actions">
<li v-if="!showHouseholdSuggestion" class="cancel">
<button
class="btn btn-misc"
@click="toggleHouseholdSuggestion"
><i class="fa fa-fw fa-caret-right"></i>
{{ $tc('household_members_editor.show_household_suggestion',
countHouseholdSuggestion) }}
</button>
</li>
<li v-if="showHouseholdSuggestion && hasHouseholdSuggestion" class="cancel">
<button
class="btn btn-misc"
@click="toggleHouseholdSuggestion"
><i class="fa fa-fw fa-caret-down"></i>
{{ $t('household_members_editor.hide_household_suggestion') }}
</button>
</li>
<li v-if="allowHouseholdCreate"> <li v-if="allowHouseholdCreate">
<button class="btn btn-create" @click="createHousehold"> <button class="btn btn-create" @click="createHousehold">
{{ $t('household_members_editor.household.create_household') }} {{ $t('household_members_editor.household.create_household') }}
@ -99,29 +121,54 @@
</li> </li>
</ul> </ul>
<div class="householdSuggestions"> <div v-if="hasHouseholdSuggestion" class="householdSuggestions my-5">
<div v-if="showHouseholdSuggestion && hasHouseholdSuggestion"> <h4 class="mb-3">
{{ $t('household_members_editor.household_for_participants_accompanying_period') }} :
<h3>{{ $t('household_members_editor.suggestions') }}</h3> </h4>
<p>{{ $t('household_members_editor.household_for_participants_accompanying_period') }}:</p> <div class="accordion" id="householdSuggestions">
<div class="accordion-item">
<div class="flex-table householdSuggestionList"> <h2 class="accordion-header" id="heading_household_suggestions">
<div v-for="h in filterHouseholdSuggestionByAccompanyingPeriod" class="item-bloc"> <button v-if="!showHouseholdSuggestion"
<household-render-box :household="h"></household-render-box> class="accordion-button collapsed"
<ul class="record_actions"> type="button"
<li> data-bs-toggle="collapse"
<button class="btn btn-sm btn-choose" @click="selectHousehold(h)"> aria-expanded="false"
{{ $t('household_members_editor.select_household') }} @click="toggleHouseholdSuggestion">
</button> {{ $tc('household_members_editor.show_household_suggestion', countHouseholdSuggestion) }}
</li> </button>
</ul> <button v-if="showHouseholdSuggestion"
</div> class="accordion-button"
type="button"
data-bs-toggle="collapse"
aria-expanded="true"
@click="toggleHouseholdSuggestion">
{{ $t('household_members_editor.hide_household_suggestion') }}
</button>
<!-- disabled bootstrap behaviour: data-bs-target="#collapse_household_suggestions" aria-controls="collapse_household_suggestions" -->
</h2>
<div class="accordion-collapse" id="collapse_household_suggestions"
aria-labelledby="heading_household_suggestions" data-bs-parent="#householdSuggestions">
<div v-if="showHouseholdSuggestion">
<div class="flex-table householdSuggestionList">
<div v-for="h in filterHouseholdSuggestionByAccompanyingPeriod" class="item-bloc">
<household-render-box :household="h"></household-render-box>
<ul class="record_actions">
<li>
<button class="btn btn-sm btn-choose" @click="selectHousehold(h)">
{{ $t('household_members_editor.select_household') }}
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div>
</div>
</div>
</template> </template>
<script> <script>
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import HouseholdRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue'; import HouseholdRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue';
@ -154,12 +201,12 @@ export default {
}, },
button: { button: {
text: { text: {
create: null, create: 'household_members_editor.household.or_create_new_address',
edit: null, edit: null,
} }
}, },
title: { title: {
create: null, create: 'household_members_editor.household.create_new_address',
edit: null, edit: null,
}, },
} }
@ -173,11 +220,14 @@ export default {
'hasHouseholdSuggestion', 'hasHouseholdSuggestion',
'countHouseholdSuggestion', 'countHouseholdSuggestion',
'filterHouseholdSuggestionByAccompanyingPeriod', 'filterHouseholdSuggestionByAccompanyingPeriod',
'hasAddressSuggestion',
'countAddressSuggestion',
'filterAddressesSuggestion', 'filterAddressesSuggestion',
'hasHouseholdAddress', 'hasHouseholdAddress',
]), ]),
...mapState([ ...mapState([
'showHouseholdSuggestion', 'showHouseholdSuggestion',
'showAddressSuggestion'
]), ]),
household() { household() {
return this.$store.state.household; return this.$store.state.household;
@ -218,8 +268,12 @@ export default {
toggleHouseholdSuggestion() { toggleHouseholdSuggestion() {
this.$store.commit('toggleHouseholdSuggestion'); this.$store.commit('toggleHouseholdSuggestion');
}, },
toggleAddressSuggestion() {
this.$store.commit('toggleAddressSuggestion');
},
selectHousehold(h) { selectHousehold(h) {
this.$store.dispatch('selectHousehold', h); this.$store.dispatch('selectHousehold', h);
this.toggleHouseholdSuggestion();
}, },
removeHousehold() { removeHousehold() {
this.$store.dispatch('removeHousehold'); this.$store.dispatch('removeHousehold');
@ -236,7 +290,7 @@ export default {
}, },
removeHouseholdAddress() { removeHouseholdAddress() {
this.$store.commit('removeHouseholdAddress'); this.$store.commit('removeHouseholdAddress');
} },
}, },
}; };
@ -246,12 +300,15 @@ export default {
div#household_members_editor div, div#household_members_editor div,
div.householdSuggestionList { div.householdSuggestionList {
&.flex-table div.item-bloc div.item-row div.item-col { &.flex-table {
&:first-child { margin: 0;
width: 25%; div.item-bloc div.item-row div.item-col {
} &:first-child {
&:last-child { width: 25%;
display: initial; }
&:last-child {
display: initial;
}
} }
} }
} }

View File

@ -7,14 +7,16 @@ const appMessages = {
household: { household: {
no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage. Les usagers concernés par la modification apparaitront ensuite.", no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage. Les usagers concernés par la modification apparaitront ensuite.",
new_household: "Nouveau ménage", new_household: "Nouveau ménage",
create_household: "Créer un ménage de destination", create_household: "Créer un nouveau ménage de destination",
search_household: "Chercher un ménage", search_household: "Chercher un ménage",
will_leave_any_household: "Ne rejoignent pas de ménage", will_leave_any_household: "Ne rejoignent pas de ménage",
leave_without_household: "Sans nouveau ménage", leave_without_household: "Sans nouveau ménage",
where_live_the_household: "À quelle adresse habite ce ménage ?", where_live_the_household: "À quelle adresse habite ce ménage ?",
household_live_to_this_address: "Le ménage habite cette adresse", household_live_to_this_address: "Sélectionner l'adresse",
no_suggestions: "Aucune adresse à suggérer", no_suggestions: "Aucune adresse à suggérer",
delete_this_address: "Supprimer cette adresse", delete_this_address: "Supprimer cette adresse",
create_new_address: "Créer une nouvelle adresse",
or_create_new_address: "Ou créer une nouvelle adresse",
}, },
concerned: { concerned: {
title: "Nouveaux membres du ménage", title: "Nouveaux membres du ménage",
@ -36,8 +38,8 @@ const appMessages = {
suggestions: "Suggestions", suggestions: "Suggestions",
hide_household_suggestion: "Masquer les suggestions", hide_household_suggestion: "Masquer les suggestions",
show_household_suggestion: 'Aucune suggestion | Afficher une suggestion | Afficher {count} suggestions', show_household_suggestion: 'Aucune suggestion | Afficher une suggestion | Afficher {count} suggestions',
household_for_participants_accompanying_period: "Ces ménages partagent le même parcours", household_for_participants_accompanying_period: "Des ménages partagent le même parcours",
select_household: "Sélectionner", select_household: "Sélectionner le ménage",
dates_title: "Période de validité", dates_title: "Période de validité",
dates: { dates: {
start_date: "Début de validité", start_date: "Début de validité",

View File

@ -36,6 +36,7 @@ const store = createStore({
householdSuggestionByAccompanyingPeriod: [], householdSuggestionByAccompanyingPeriod: [],
showHouseholdSuggestion: window.household_members_editor_expand_suggestions === 1, showHouseholdSuggestion: window.household_members_editor_expand_suggestions === 1,
addressesSuggestion: [], addressesSuggestion: [],
showAddressSuggestion: true,
warnings: [], warnings: [],
errors: [] errors: []
}, },
@ -73,6 +74,12 @@ const store = createStore({
.filter(h => h.id !== state.household.id) .filter(h => h.id !== state.household.id)
; ;
}, },
hasAddressSuggestion(state, getters) {
return getters.filterAddressesSuggestion.length > 0;
},
countAddressSuggestion(state, getters) {
return getters.filterAddressesSuggestion.length;
},
filterAddressesSuggestion(state) { filterAddressesSuggestion(state) {
if (state.household === null) { if (state.household === null) {
return state.addressesSuggestion; return state.addressesSuggestion;
@ -260,6 +267,9 @@ const store = createStore({
toggleHouseholdSuggestion(state) { toggleHouseholdSuggestion(state) {
state.showHouseholdSuggestion = !state.showHouseholdSuggestion; state.showHouseholdSuggestion = !state.showHouseholdSuggestion;
}, },
toggleAddressSuggestion(state) {
state.showAddressSuggestion = !state.showAddressSuggestion;
},
setWarnings(state, warnings) { setWarnings(state, warnings) {
state.warnings = warnings; state.warnings = warnings;
// reset errors, which should come from servers // reset errors, which should come from servers
@ -327,7 +337,7 @@ const store = createStore({
commit('forceLeaveWithoutHousehold'); commit('forceLeaveWithoutHousehold');
dispatch('computeWarnings'); dispatch('computeWarnings');
}, },
selectHousehold({ commit }, h) { selectHousehold({ commit, dispatch }, h) {
commit('selectHousehold', h); commit('selectHousehold', h);
dispatch('computeWarnings'); dispatch('computeWarnings');
}, },

View File

@ -115,6 +115,17 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
'icon' => 'tasks' 'icon' => 'tasks'
]); ]);
$menu->addChild("My calendar list", [
'route' => 'chill_calendar_calendar_list',
'routeParameters' => [
'user_id' => $user->getId(),
]
])
->setExtras([
'order' => -9,
'icon' => 'tasks'
]);
/* /*
$menu->addChild("My aside activities", [ $menu->addChild("My aside activities", [
'route' => 'chill_crud_aside_activity_index' 'route' => 'chill_crud_aside_activity_index'

@ -1 +1 @@
Subproject commit 8839b431f296733b792c788f2ef58e5ecd6419d3 Subproject commit bd95d3c96a437757b7e8f35cdfd30da9aeac1a01