mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-25 16:14:59 +00:00
Compare commits
1 Commits
v4.3.0
...
385-invita
Author | SHA1 | Date | |
---|---|---|---|
95d9a75e46 |
6
.changes/unreleased/Feature-20250808-120802.yaml
Normal file
6
.changes/unreleased/Feature-20250808-120802.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: Create invitation list in user menu
|
||||||
|
time: 2025-08-08T12:08:02.446361367+02:00
|
||||||
|
custom:
|
||||||
|
Issue: "385"
|
||||||
|
SchemaChange: No schema change
|
@@ -1,10 +0,0 @@
|
|||||||
## v4.3.0 - 2025-09-08
|
|
||||||
### Feature
|
|
||||||
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
|
|
||||||
* Add a command to generate a list of permissions
|
|
||||||
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
|
|
||||||
|
|
||||||
**Schema Change**: Add columns or tables
|
|
||||||
### Fixed
|
|
||||||
* fix date formatting in calendar range display
|
|
||||||
* Change route URL to avoid clash with person duplicate controller method
|
|
11
CHANGELOG.md
11
CHANGELOG.md
@@ -6,17 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
|||||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||||
|
|
||||||
|
|
||||||
## v4.3.0 - 2025-09-08
|
|
||||||
### Feature
|
|
||||||
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
|
|
||||||
* Add a command to generate a list of permissions
|
|
||||||
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
|
|
||||||
|
|
||||||
**Schema Change**: Add columns or tables
|
|
||||||
### Fixed
|
|
||||||
* fix date formatting in calendar range display
|
|
||||||
* Change route URL to avoid clash with person duplicate controller method
|
|
||||||
|
|
||||||
## v4.2.1 - 2025-09-03
|
## v4.2.1 - 2025-09-03
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fix exports to work with DirectExportInterface
|
* Fix exports to work with DirectExportInterface
|
||||||
|
@@ -266,7 +266,7 @@ class CalendarController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->getUser() instanceof User) {
|
if (!$this->getUser() instanceof User) {
|
||||||
throw new UnauthorizedHttpException('you are not an user');
|
throw new UnauthorizedHttpException('you are not a user');
|
||||||
}
|
}
|
||||||
|
|
||||||
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
|
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
|
||||||
|
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\CalendarBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\CalendarBundle\Repository\InviteACLAwareRepository;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
|
use Doctrine\ORM\NoResultException;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
class MyInvitationsController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(private readonly InviteACLAwareRepository $inviteACLAwareRepository, private readonly PaginatorFactory $paginator) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NonUniqueResultException
|
||||||
|
* @throws NoResultException
|
||||||
|
*/
|
||||||
|
#[Route(path: '/{_locale}/calendar/invitations/my', name: 'chill_calendar_invitations_list_my')]
|
||||||
|
public function myInvitations(Request $request): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||||
|
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw new UnauthorizedHttpException('you are not a user');
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = $this->inviteACLAwareRepository->countByUser($user);
|
||||||
|
$paginator = $this->paginator->create($total);
|
||||||
|
$invitations = $this->inviteACLAwareRepository->findByUser(
|
||||||
|
$user,
|
||||||
|
['createdAt' => 'DESC'],
|
||||||
|
$paginator->getCurrentPageFirstItemNumber(),
|
||||||
|
$paginator->getItemsPerPage()
|
||||||
|
);
|
||||||
|
|
||||||
|
dump($invitations);
|
||||||
|
|
||||||
|
$view = '@ChillCalendar/Invitations/listByUser.html.twig';
|
||||||
|
|
||||||
|
return $this->render($view, [
|
||||||
|
'invitations' => $invitations,
|
||||||
|
'paginator' => $paginator,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@@ -30,6 +30,13 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
|
|||||||
'order' => 9,
|
'order' => 9,
|
||||||
'icon' => 'tasks',
|
'icon' => 'tasks',
|
||||||
]);
|
]);
|
||||||
|
$menu->addChild('My invitations list', [
|
||||||
|
'route' => 'chill_calendar_invitations_list_my',
|
||||||
|
])
|
||||||
|
->setExtras([
|
||||||
|
'order' => 9,
|
||||||
|
'icon' => 'tasks',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\CalendarBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\CalendarBundle\Entity\Invite;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
|
use Doctrine\ORM\NoResultException;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
|
||||||
|
readonly class InviteACLAwareRepository implements InviteACLAwareRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(private EntityManagerInterface $em) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NonUniqueResultException
|
||||||
|
* @throws NoResultException
|
||||||
|
*/
|
||||||
|
public function countByUser(User $user): int
|
||||||
|
{
|
||||||
|
return $this->buildQueryByUser($user)
|
||||||
|
->select('COUNT(i)')
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByUser(User $user, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array
|
||||||
|
{
|
||||||
|
$qb = $this->buildQueryByUser($user)
|
||||||
|
->select('i');
|
||||||
|
|
||||||
|
foreach ($orderBy as $sort => $order) {
|
||||||
|
$qb->addOrderBy('i.'.$sort, $order);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $offset) {
|
||||||
|
$qb->setFirstResult($offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $limit) {
|
||||||
|
$qb->setMaxResults($limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildQueryByUser(User $user): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->em->createQueryBuilder()
|
||||||
|
->from(Invite::class, 'i');
|
||||||
|
|
||||||
|
$qb->where('i.user = :user');
|
||||||
|
|
||||||
|
$qb->setParameter('user', $user);
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\CalendarBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
|
||||||
|
interface InviteACLAwareRepositoryInterface
|
||||||
|
{
|
||||||
|
public function countByUser(User $user): int;
|
||||||
|
|
||||||
|
public function findByUser(User $user, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array;
|
||||||
|
}
|
@@ -70,8 +70,6 @@
|
|||||||
<option value="00:10:00">10 minutes</option>
|
<option value="00:10:00">10 minutes</option>
|
||||||
<option value="00:15:00">15 minutes</option>
|
<option value="00:15:00">15 minutes</option>
|
||||||
<option value="00:30:00">30 minutes</option>
|
<option value="00:30:00">30 minutes</option>
|
||||||
<option value="00:45:00">45 minutes</option>
|
|
||||||
<option value="00:60:00">60 minutes</option>
|
|
||||||
</select>
|
</select>
|
||||||
<label class="input-group-text" for="slotMinTime">De</label>
|
<label class="input-group-text" for="slotMinTime">De</label>
|
||||||
<select
|
<select
|
||||||
|
@@ -32,8 +32,6 @@
|
|||||||
<option value="00:10:00">10 minutes</option>
|
<option value="00:10:00">10 minutes</option>
|
||||||
<option value="00:15:00">15 minutes</option>
|
<option value="00:15:00">15 minutes</option>
|
||||||
<option value="00:30:00">30 minutes</option>
|
<option value="00:30:00">30 minutes</option>
|
||||||
<option value="00:45:00">45 minutes</option>
|
|
||||||
<option value="00:60:00">60 minutes</option>
|
|
||||||
</select>
|
</select>
|
||||||
<label class="input-group-text" for="slotMinTime">De</label>
|
<label class="input-group-text" for="slotMinTime">De</label>
|
||||||
<select
|
<select
|
||||||
@@ -104,8 +102,7 @@
|
|||||||
event.title
|
event.title
|
||||||
}}</b>
|
}}</b>
|
||||||
<b v-else-if="event.extendedProps.is === 'range'"
|
<b v-else-if="event.extendedProps.is === 'range'"
|
||||||
>{{ formatDate(event.startStr, "time") }} -
|
>{{ formatDate(event.startStr) }} -
|
||||||
{{ formatDate(event.endStr, "time") }}:
|
|
||||||
{{ event.extendedProps.locationName }}</b
|
{{ event.extendedProps.locationName }}</b
|
||||||
>
|
>
|
||||||
<b v-else-if="event.extendedProps.is === 'local'">{{
|
<b v-else-if="event.extendedProps.is === 'local'">{{
|
||||||
@@ -297,26 +294,9 @@ const nextWeeks = computed((): Weeks[] =>
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatDate = (datetime: string, format: null | "time" = null) => {
|
const formatDate = (datetime: string) => {
|
||||||
const date = ISOToDate(datetime);
|
console.log(typeof datetime);
|
||||||
if (!date) return "";
|
return ISOToDate(datetime);
|
||||||
|
|
||||||
if (format === "time") {
|
|
||||||
return date.toLocaleTimeString("fr-FR", {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// French date formatting
|
|
||||||
return date.toLocaleDateString("fr-FR", {
|
|
||||||
weekday: "short",
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseOptions = ref<CalendarOptions>({
|
const baseOptions = ref<CalendarOptions>({
|
||||||
|
@@ -0,0 +1,172 @@
|
|||||||
|
{% if invitations|length > 0 %}
|
||||||
|
<div class="flex-table list-records">
|
||||||
|
|
||||||
|
{% for invitation in invitations %}
|
||||||
|
{% set calendar = invitation.getCalendar %}
|
||||||
|
|
||||||
|
{% if calendar is not null %}
|
||||||
|
<div class="item-bloc">
|
||||||
|
<div class="item-row main">
|
||||||
|
<div class="item-col">
|
||||||
|
<div class="wrap-header">
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">
|
||||||
|
<p class="date-label">
|
||||||
|
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
|
||||||
|
{{ calendar.startDate|format_datetime('short', 'short') }}
|
||||||
|
- {{ calendar.endDate|format_datetime('short', 'short') }}
|
||||||
|
{% else %}
|
||||||
|
{{ calendar.startDate|format_datetime('short', 'short') }}
|
||||||
|
- {{ calendar.endDate|format_datetime('none', 'short') }}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="duration short-message">
|
||||||
|
<i class="fa fa-fw fa-hourglass-end"></i>
|
||||||
|
{{ calendar.duration|date('%H:%I') }}
|
||||||
|
{% if false == calendar.sendSMS or null == calendar.sendSMS %}
|
||||||
|
<!-- no sms will be send -->
|
||||||
|
{% else %}
|
||||||
|
{% if calendar.smsStatus == 'sms_sent' %}
|
||||||
|
<span title="{{ 'SMS already sent'|trans }}" class="badge bg-info">
|
||||||
|
<i class="fa fa-check "></i>
|
||||||
|
<i class="fa fa-envelope "></i>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span title="{{ 'Will send SMS'|trans }}" class="badge bg-info">
|
||||||
|
<i class="fa fa-envelope "></i>
|
||||||
|
<i class="fa fa-hourglass-end "></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-col">
|
||||||
|
<ul class="list-content">
|
||||||
|
{% if calendar.mainUser is not empty %}
|
||||||
|
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</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 separator">
|
||||||
|
<div class="item-col">
|
||||||
|
{% include '@ChillActivity/Activity/concernedGroups.html.twig' with {
|
||||||
|
'context': calendar.context == 'person' ? 'calendar_person' : 'calendar_accompanyingCourse',
|
||||||
|
'render': 'wrap-list',
|
||||||
|
'entity': calendar
|
||||||
|
} %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if calendar.comment.comment is not empty %}
|
||||||
|
<div class="item-row details separator">
|
||||||
|
<div class="item-col comment">
|
||||||
|
{{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if calendar.location is not empty %}
|
||||||
|
<div class="item-row separator">
|
||||||
|
<div>
|
||||||
|
{% if calendar.location.address is not same as(null) and calendar.location.name is not empty %}
|
||||||
|
<i class="fa fa-map-marker"></i>{% endif %}
|
||||||
|
{% if calendar.location.name is not empty %}{{ calendar.location.name }}{% endif %}
|
||||||
|
{% if calendar.location.address is not same as(null) %}{{ calendar.location.address|chill_entity_render_box({'multiline': false, 'with_picto': (calendar.location.name is empty)}) }}{% else %}
|
||||||
|
<i class="fa fa-map-marker"></i>{% endif %}
|
||||||
|
{% if calendar.location.phonenumber1 is not empty %}<i
|
||||||
|
class="fa fa-phone"></i> {{ calendar.location.phonenumber1|chill_format_phonenumber }}{% endif %}
|
||||||
|
{% if calendar.location.phonenumber2 is not empty %}<i
|
||||||
|
class="fa fa-phone"></i> {{ calendar.location.phonenumber2|chill_format_phonenumber }}{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="item-row separator column">
|
||||||
|
<div>
|
||||||
|
|
||||||
|
{{ include('@ChillCalendar/Calendar/_documents.twig.html') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if calendar.activity is not null %}
|
||||||
|
<div class="item-row separator">
|
||||||
|
<div class="item-col">
|
||||||
|
<div class="wrap-list">
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title"><h3>{{ 'Activity'|trans }}</h3></div>
|
||||||
|
<div class="wl-col list activity-linked">
|
||||||
|
<h2 class="badge-title">
|
||||||
|
<span class="title_label"></span>
|
||||||
|
<span class="title_action">
|
||||||
|
{{ calendar.activity.type.name | localize_translatable_string }}
|
||||||
|
|
||||||
|
{% if calendar.activity.emergency %}
|
||||||
|
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li class="cancel">
|
||||||
|
<span class="createdBy">
|
||||||
|
{{ 'Created by'|trans }}
|
||||||
|
<b>{{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': calendar.activity.id}) }}" class="btn btn-sm btn-show" ></a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="item-row separator">
|
||||||
|
<ul class="record_actions">
|
||||||
|
|
||||||
|
{% if (calendar.isInvited(app.user)) %}
|
||||||
|
{% set invite = calendar.inviteForUser(app.user) %}
|
||||||
|
<li>
|
||||||
|
<div invite-answer data-status="{{ invite.status|e('html_attr') }}"
|
||||||
|
data-calendar-id="{{ calendar.id|e('html_attr') }}"></div>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_show', { 'id': calendar.id}) }}"
|
||||||
|
class="btn btn-show "></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if invitations|length < paginator.getTotalItems %}
|
||||||
|
{{ chill_pagination(paginator) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
@@ -0,0 +1,27 @@
|
|||||||
|
{% extends "@ChillMain/layout.html.twig" %}
|
||||||
|
|
||||||
|
{% set activeRouteKey = 'chill_calendar_invitations_list' %}
|
||||||
|
|
||||||
|
{% block title %}{{ 'My invitations list' |trans }}{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>{{ 'Invitation list' |trans }}</h1>
|
||||||
|
|
||||||
|
{% if invitations|length == 0 %}
|
||||||
|
<p class="chill-no-data-statement">
|
||||||
|
{{ "There is no invitation items."|trans }}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
{{ include ('@ChillCalendar/Invitations/_list_item.html.twig') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ parent() }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ parent() }}
|
||||||
|
{% endblock %}
|
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Command;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Security\RoleDumper;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
|
|
||||||
#[AsCommand(name: 'chill:main:dump-list-permissions', description: 'Print a markdown reference of permissions (roles) grouped by title with dependencies).')]
|
|
||||||
final class DumpListPermissionsCommand extends Command
|
|
||||||
{
|
|
||||||
public function __construct(private readonly RoleDumper $roleDumper)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$markdown = $this->roleDumper->dumpAsMarkdown();
|
|
||||||
$output->writeln($markdown);
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -48,7 +48,6 @@ class AbsenceController extends AbstractController
|
|||||||
$user = $this->security->getUser();
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
$user->setAbsenceStart(null);
|
$user->setAbsenceStart(null);
|
||||||
$user->setAbsenceEnd(null);
|
|
||||||
$em = $this->managerRegistry->getManager();
|
$em = $this->managerRegistry->getManager();
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
|
@@ -24,7 +24,6 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
|||||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User.
|
* User.
|
||||||
@@ -46,8 +45,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||||
private ?\DateTimeImmutable $absenceStart = null;
|
private ?\DateTimeImmutable $absenceStart = null;
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
|
||||||
private ?\DateTimeImmutable $absenceEnd = null;
|
|
||||||
/**
|
/**
|
||||||
* Array where SAML attributes's data are stored.
|
* Array where SAML attributes's data are stored.
|
||||||
*/
|
*/
|
||||||
@@ -160,11 +157,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
return $this->absenceStart;
|
return $this->absenceStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAbsenceEnd(): ?\DateTimeImmutable
|
|
||||||
{
|
|
||||||
return $this->absenceEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get attributes.
|
* Get attributes.
|
||||||
*
|
*
|
||||||
@@ -344,13 +336,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
|
|
||||||
public function isAbsent(): bool
|
public function isAbsent(): bool
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable('now');
|
return null !== $this->getAbsenceStart() && $this->getAbsenceStart() <= new \DateTimeImmutable('now');
|
||||||
$absenceStart = $this->getAbsenceStart();
|
|
||||||
$absenceEnd = $this->getAbsenceEnd();
|
|
||||||
|
|
||||||
return null !== $absenceStart
|
|
||||||
&& $absenceStart <= $now
|
|
||||||
&& (null === $absenceEnd || $now <= $absenceEnd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -424,11 +410,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
$this->absenceStart = $absenceStart;
|
$this->absenceStart = $absenceStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setAbsenceEnd(?\DateTimeImmutable $absenceEnd): void
|
|
||||||
{
|
|
||||||
$this->absenceEnd = $absenceEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setAttributeByDomain(string $domain, string $key, $value): self
|
public function setAttributeByDomain(string $domain, string $key, $value): self
|
||||||
{
|
{
|
||||||
$this->attributes[$domain][$key] = $value;
|
$this->attributes[$domain][$key] = $value;
|
||||||
@@ -694,16 +675,4 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
{
|
{
|
||||||
return 'fr';
|
return 'fr';
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Assert\Callback]
|
|
||||||
public function validateAbsenceDates(ExecutionContextInterface $context): void
|
|
||||||
{
|
|
||||||
if (null !== $this->getAbsenceEnd() && null === $this->getAbsenceStart()) {
|
|
||||||
$context->buildViolation(
|
|
||||||
'user.absence_end_requires_start'
|
|
||||||
)
|
|
||||||
->atPath('absenceEnd')
|
|
||||||
->addViolation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@ use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
|||||||
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter?: string, formatter: array{form: array<string, mixed>, version: int}}
|
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter: string, formatter: array{form: array<string, mixed>, version: int}}
|
||||||
*/
|
*/
|
||||||
class ExportConfigNormalizer
|
class ExportConfigNormalizer
|
||||||
{
|
{
|
||||||
|
@@ -23,14 +23,9 @@ class AbsenceType extends AbstractType
|
|||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('absenceStart', ChillDateType::class, [
|
->add('absenceStart', ChillDateType::class, [
|
||||||
'required' => false,
|
'required' => true,
|
||||||
'input' => 'datetime_immutable',
|
'input' => 'datetime_immutable',
|
||||||
'label' => 'absence.Absence start',
|
'label' => 'absence.Absence start',
|
||||||
])
|
|
||||||
->add('absenceEnd', ChillDateType::class, [
|
|
||||||
'required' => false,
|
|
||||||
'input' => 'datetime_immutable',
|
|
||||||
'label' => 'absence.Absence end',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -105,11 +105,6 @@ class UserType extends AbstractType
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
'input' => 'datetime_immutable',
|
'input' => 'datetime_immutable',
|
||||||
'label' => 'absence.Absence start',
|
'label' => 'absence.Absence start',
|
||||||
])
|
|
||||||
->add('absenceEnd', ChillDateType::class, [
|
|
||||||
'required' => false,
|
|
||||||
'input' => 'datetime_immutable',
|
|
||||||
'label' => 'absence.Absence end',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// @phpstan-ignore-next-line
|
// @phpstan-ignore-next-line
|
||||||
|
@@ -37,13 +37,8 @@ export const ISOToDate = (str: string | null): Date | null => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the string already contains time info, use it directly
|
|
||||||
if (str.includes("T") || str.includes(" ")) {
|
|
||||||
return new Date(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, parse date only
|
|
||||||
const [year, month, day] = str.split("-").map((p) => parseInt(p));
|
const [year, month, day] = str.split("-").map((p) => parseInt(p));
|
||||||
|
|
||||||
return new Date(year, month - 1, day, 0, 0, 0, 0);
|
return new Date(year, month - 1, day, 0, 0, 0, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,19 +69,20 @@ export const ISOToDatetime = (str: string | null): Date | null => {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const datetimeToISO = (date: Date): string => {
|
export const datetimeToISO = (date: Date): string => {
|
||||||
const cal = [
|
let cal, time, offset;
|
||||||
|
cal = [
|
||||||
date.getFullYear(),
|
date.getFullYear(),
|
||||||
(date.getMonth() + 1).toString().padStart(2, "0"),
|
(date.getMonth() + 1).toString().padStart(2, "0"),
|
||||||
date.getDate().toString().padStart(2, "0"),
|
date.getDate().toString().padStart(2, "0"),
|
||||||
].join("-");
|
].join("-");
|
||||||
|
|
||||||
const time = [
|
time = [
|
||||||
date.getHours().toString().padStart(2, "0"),
|
date.getHours().toString().padStart(2, "0"),
|
||||||
date.getMinutes().toString().padStart(2, "0"),
|
date.getMinutes().toString().padStart(2, "0"),
|
||||||
date.getSeconds().toString().padStart(2, "0"),
|
date.getSeconds().toString().padStart(2, "0"),
|
||||||
].join(":");
|
].join(":");
|
||||||
|
|
||||||
const offset = [
|
offset = [
|
||||||
date.getTimezoneOffset() <= 0 ? "+" : "-",
|
date.getTimezoneOffset() <= 0 ? "+" : "-",
|
||||||
Math.abs(Math.floor(date.getTimezoneOffset() / 60))
|
Math.abs(Math.floor(date.getTimezoneOffset() / 60))
|
||||||
.toString()
|
.toString()
|
||||||
|
@@ -8,36 +8,36 @@
|
|||||||
|
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<h2>{{ 'absence.My absence'|trans }}</h2>
|
<h2>{{ 'absence.My absence'|trans }}</h2>
|
||||||
<div>
|
|
||||||
{% if user.absenceStart is not null %}
|
|
||||||
<div class="alert alert-success flash_message">{{ 'absence.You are listed as absent, as of {date, date, short}'|trans({
|
|
||||||
date: user.absenceStart
|
|
||||||
}) }}
|
|
||||||
{% if user.absenceEnd is not null %}
|
|
||||||
{{ 'until %date%'|trans({'%date%': user.absenceEnd|format_date('short') }) }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="alert alert-warning flash_message">{{ 'absence.No absence listed'|trans }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ form_start(form) }}
|
|
||||||
{{ form_row(form.absenceStart) }}
|
|
||||||
{{ form_row(form.absenceEnd) }}
|
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
{% if user.absenceStart is not null %}
|
||||||
<li>
|
<div>
|
||||||
<a class="btn btn-delete" title="Modifier" href="{{ path('chill_main_user_absence_unset') }}">{{ 'absence.Unset absence'|trans }}</a>
|
<p>{{ 'absence.You are listed as absent, as of'|trans }} {{ user.absenceStart|format_date('long') }}</p>
|
||||||
</li>
|
<ul class="record_actions sticky-form-buttons">
|
||||||
<li>
|
<li>
|
||||||
<button class="btn btn-save" type="submit">
|
<a href="{{ path('chill_main_user_absence_unset') }}"
|
||||||
{{ 'Save'|trans }}
|
class="btn btn-delete">{{ 'absence.Unset absence'|trans }}</a>
|
||||||
</button>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
{{ form_end(form) }}
|
{% else %}
|
||||||
</div>
|
<div>
|
||||||
|
<p class="chill-no-data-statement">{{ 'absence.No absence listed'|trans }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ form_start(form) }}
|
||||||
|
{{ form_row(form.absenceStart) }}
|
||||||
|
|
||||||
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
<li>
|
||||||
|
<button class="btn btn-save" type="submit">
|
||||||
|
{{ 'Save'|trans }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -79,7 +79,7 @@
|
|||||||
<div class="d-flex flex-row mb-5 alert alert-warning" role="alert">
|
<div class="d-flex flex-row mb-5 alert alert-warning" role="alert">
|
||||||
<p class="m-2">{{'absence.You are marked as being absent'|trans }}</p>
|
<p class="m-2">{{'absence.You are marked as being absent'|trans }}</p>
|
||||||
<span class="ms-auto">
|
<span class="ms-auto">
|
||||||
<a class="btn btn-delete" title="Modifier" href="{{ path('chill_main_user_absence_unset') }}">{{ 'absence.Unset absence'|trans }}</a>
|
<a class="btn btn-remove" title="Modifier" href="{{ path('chill_main_user_absence_index') }}">{{ 'absence.Unset absence'|trans }}</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -1,86 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Security;
|
|
||||||
|
|
||||||
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
||||||
|
|
||||||
final readonly class RoleDumper
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private RoleProvider $roleProvider,
|
|
||||||
private RoleHierarchyInterface $roleHierarchy,
|
|
||||||
private TranslatorInterface $translator,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function dumpAsMarkdown(): string
|
|
||||||
{
|
|
||||||
$roles = $this->roleProvider->getRoles();
|
|
||||||
$rolesWithoutScopes = $this->roleProvider->getRolesWithoutScopes();
|
|
||||||
|
|
||||||
// Group roles by title
|
|
||||||
$groups = [];
|
|
||||||
foreach ($roles as $role) {
|
|
||||||
$title = $this->roleProvider->getRoleTitle($role);
|
|
||||||
$title ??= 'Other';
|
|
||||||
$groups[$title][] = $role;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort groups by title
|
|
||||||
ksort($groups, SORT_NATURAL | SORT_FLAG_CASE);
|
|
||||||
|
|
||||||
$lines = [];
|
|
||||||
foreach ($groups as $title => $roleList) {
|
|
||||||
// Sort roles by translated label for deterministic output
|
|
||||||
usort($roleList, function (string $a, string $b): int {
|
|
||||||
$ta = $this->translator->trans($a);
|
|
||||||
$tb = $this->translator->trans($b);
|
|
||||||
|
|
||||||
return strcasecmp($ta, $tb);
|
|
||||||
});
|
|
||||||
|
|
||||||
$translatedTitle = $this->translator->trans($title);
|
|
||||||
$lines[] = '## '.$translatedTitle;
|
|
||||||
|
|
||||||
foreach ($roleList as $role) {
|
|
||||||
// Translate primary role
|
|
||||||
$translatedRole = $this->translator->trans($role);
|
|
||||||
|
|
||||||
// Scope marker: (S) if needs scope, (~~S~~) if no scope required
|
|
||||||
$needsScope = !in_array($role, $rolesWithoutScopes, true);
|
|
||||||
$scopeMarker = $needsScope ? '(S)' : '(~~S~~)';
|
|
||||||
|
|
||||||
// Compute dependent roles from hierarchy (exclude itself)
|
|
||||||
$reachable = $this->roleHierarchy->getReachableRoleNames([$role]);
|
|
||||||
$dependents = array_values(array_filter($reachable, static fn (string $r): bool => $r !== $role));
|
|
||||||
|
|
||||||
// Translate dependents and sort deterministically
|
|
||||||
$translatedDependents = array_map(fn (string $r) => $this->translator->trans($r), $dependents);
|
|
||||||
sort($translatedDependents, SORT_NATURAL | SORT_FLAG_CASE);
|
|
||||||
|
|
||||||
if (count($translatedDependents) > 0) {
|
|
||||||
$lines[] = sprintf('- **%s** %s: %s', $translatedRole, $scopeMarker, implode(', ', $translatedDependents));
|
|
||||||
} else {
|
|
||||||
$lines[] = sprintf('- **%s** %s', $translatedRole, $scopeMarker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a blank line between groups
|
|
||||||
$lines[] = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim possible trailing blank line
|
|
||||||
$markdown = rtrim(implode("\n", $lines));
|
|
||||||
|
|
||||||
return $markdown."\n"; // End with newline for POSIX friendliness
|
|
||||||
}
|
|
||||||
}
|
|
@@ -52,8 +52,12 @@ class RoleProvider
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the title for each role.
|
* Get the title for each role.
|
||||||
|
*
|
||||||
|
* @param string $role
|
||||||
|
*
|
||||||
|
* @return string the title of the role
|
||||||
*/
|
*/
|
||||||
public function getRoleTitle(string $role): ?string
|
public function getRoleTitle($role)
|
||||||
{
|
{
|
||||||
$this->initializeRolesTitlesCache();
|
$this->initializeRolesTitlesCache();
|
||||||
|
|
||||||
@@ -69,7 +73,7 @@ class RoleProvider
|
|||||||
/**
|
/**
|
||||||
* initialize the array for caching role and titles.
|
* initialize the array for caching role and titles.
|
||||||
*/
|
*/
|
||||||
private function initializeRolesTitlesCache(): void
|
private function initializeRolesTitlesCache()
|
||||||
{
|
{
|
||||||
// break if already initialized
|
// break if already initialized
|
||||||
if (null !== $this->rolesTitlesCache) {
|
if (null !== $this->rolesTitlesCache) {
|
||||||
|
@@ -39,8 +39,6 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
'label' => '',
|
'label' => '',
|
||||||
'email' => '',
|
'email' => '',
|
||||||
'isAbsent' => false,
|
'isAbsent' => false,
|
||||||
'absenceStart' => null,
|
|
||||||
'absenceEnd' => null,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) {}
|
public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) {}
|
||||||
@@ -79,11 +77,6 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
['docgen:expects' => PhoneNumber::class, 'groups' => 'docgen:read']
|
['docgen:expects' => PhoneNumber::class, 'groups' => 'docgen:read']
|
||||||
);
|
);
|
||||||
|
|
||||||
$absenceDatesContext = array_merge(
|
|
||||||
$context,
|
|
||||||
['docgen:expects' => \DateTimeImmutable::class, 'groups' => 'docgen:read']
|
|
||||||
);
|
|
||||||
|
|
||||||
if (null === $object && 'docgen' === $format) {
|
if (null === $object && 'docgen' === $format) {
|
||||||
return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)];
|
return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)];
|
||||||
}
|
}
|
||||||
@@ -106,8 +99,6 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
|
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
|
||||||
'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext),
|
'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext),
|
||||||
'isAbsent' => $object->isAbsent(),
|
'isAbsent' => $object->isAbsent(),
|
||||||
'absenceStart' => $this->normalizer->normalize($object->getAbsenceStart(), $format, $absenceDatesContext),
|
|
||||||
'absenceEnd' => $this->normalizer->normalize($object->getAbsenceEnd(), $format, $absenceDatesContext),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if ('docgen' === $format) {
|
if ('docgen' === $format) {
|
||||||
|
@@ -67,36 +67,4 @@ class UserTest extends TestCase
|
|||||||
->first()->getEndDate()
|
->first()->getEndDate()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIsAbsent()
|
|
||||||
{
|
|
||||||
$user = new User();
|
|
||||||
|
|
||||||
// Absent: today is within absence period
|
|
||||||
$absenceStart = new \DateTimeImmutable('-1 day');
|
|
||||||
$absenceEnd = new \DateTimeImmutable('+1 day');
|
|
||||||
$user->setAbsenceStart($absenceStart);
|
|
||||||
$user->setAbsenceEnd($absenceEnd);
|
|
||||||
self::assertTrue($user->isAbsent(), 'Should be absent when now is between start and end');
|
|
||||||
|
|
||||||
// Absent: end is null
|
|
||||||
$user->setAbsenceStart(new \DateTimeImmutable('-2 days'));
|
|
||||||
$user->setAbsenceEnd(null);
|
|
||||||
self::assertTrue($user->isAbsent(), 'Should be absent when started and no end');
|
|
||||||
|
|
||||||
// Not absent: absenceStart is in the future
|
|
||||||
$user->setAbsenceStart(new \DateTimeImmutable('+2 days'));
|
|
||||||
$user->setAbsenceEnd(null);
|
|
||||||
self::assertFalse($user->isAbsent(), 'Should not be absent if start is in the future');
|
|
||||||
|
|
||||||
// Not absent: absenceEnd is in the past
|
|
||||||
$user->setAbsenceStart(new \DateTimeImmutable('-5 days'));
|
|
||||||
$user->setAbsenceEnd(new \DateTimeImmutable('-1 day'));
|
|
||||||
self::assertFalse($user->isAbsent(), 'Should not be absent if end is in the past');
|
|
||||||
|
|
||||||
// Not absent: both are null
|
|
||||||
$user->setAbsenceStart(null);
|
|
||||||
$user->setAbsenceEnd(null);
|
|
||||||
self::assertFalse($user->isAbsent(), 'Should not be absent if start is null');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,98 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Tests\Security;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
|
||||||
use Chill\MainBundle\Security\RoleDumper;
|
|
||||||
use Chill\MainBundle\Security\RoleProvider;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class RoleDumperTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testDumpAsMarkdownGroupsByTitleTranslatesAndListsDependencies(): void
|
|
||||||
{
|
|
||||||
// Fake provider with two groups
|
|
||||||
$provider = new class () implements ProvideRoleHierarchyInterface {
|
|
||||||
public const R_PERSON_SEE = 'CHILL_PERSON_SEE';
|
|
||||||
public const R_PERSON_UPDATE = 'CHILL_PERSON_UPDATE';
|
|
||||||
public const R_REPORT_SEE = 'CHILL_REPORT_SEE';
|
|
||||||
|
|
||||||
public function getRoles(): array
|
|
||||||
{
|
|
||||||
return [self::R_PERSON_SEE, self::R_PERSON_UPDATE, self::R_REPORT_SEE];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRolesWithoutScope(): array
|
|
||||||
{
|
|
||||||
// In this test, assume REPORT_SEE does not need scope, others do
|
|
||||||
return [self::R_REPORT_SEE];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRolesWithHierarchy(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'Person' => [self::R_PERSON_SEE, self::R_PERSON_UPDATE],
|
|
||||||
'Report' => [self::R_REPORT_SEE],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$roleProvider = new RoleProvider([$provider]);
|
|
||||||
|
|
||||||
// Fake role hierarchy: UPDATE implies SEE; others none
|
|
||||||
$roleHierarchy = new class () implements RoleHierarchyInterface {
|
|
||||||
public function getReachableRoleNames(array $roles): array
|
|
||||||
{
|
|
||||||
$output = [];
|
|
||||||
foreach ($roles as $r) {
|
|
||||||
$output[] = $r;
|
|
||||||
if ('CHILL_PERSON_UPDATE' === $r) {
|
|
||||||
$output[] = 'CHILL_PERSON_SEE';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_values(array_unique($output));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fake translator that clearly shows translation applied
|
|
||||||
$translator = new class () implements TranslatorInterface {
|
|
||||||
public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
|
|
||||||
{
|
|
||||||
return 'T('.$id.')';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLocale(): string
|
|
||||||
{
|
|
||||||
return 'en';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$dumper = new RoleDumper($roleProvider, $roleHierarchy, $translator);
|
|
||||||
$md = $dumper->dumpAsMarkdown();
|
|
||||||
|
|
||||||
$expected = "## T(Person)\n"
|
|
||||||
."- **T(CHILL_PERSON_SEE)** (S)\n"
|
|
||||||
."- **T(CHILL_PERSON_UPDATE)** (S): T(CHILL_PERSON_SEE)\n\n"
|
|
||||||
."## T(Report)\n"
|
|
||||||
."- **T(CHILL_REPORT_SEE)** (~~S~~)\n";
|
|
||||||
|
|
||||||
self::assertSame($expected, $md);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -101,8 +101,6 @@ final class UserNormalizerTest extends TestCase
|
|||||||
'text_without_absent' => 'SomeUser',
|
'text_without_absent' => 'SomeUser',
|
||||||
'isAbsent' => false,
|
'isAbsent' => false,
|
||||||
'main_center' => ['context' => Center::class],
|
'main_center' => ['context' => Center::class],
|
||||||
'absenceStart' => ['context' => \DateTimeImmutable::class],
|
|
||||||
'absenceEnd' => ['context' => \DateTimeImmutable::class],
|
|
||||||
]];
|
]];
|
||||||
|
|
||||||
yield [$userNoPhone, 'docgen', ['docgen:expects' => User::class],
|
yield [$userNoPhone, 'docgen', ['docgen:expects' => User::class],
|
||||||
@@ -122,8 +120,6 @@ final class UserNormalizerTest extends TestCase
|
|||||||
'text_without_absent' => 'AnotherUser',
|
'text_without_absent' => 'AnotherUser',
|
||||||
'isAbsent' => false,
|
'isAbsent' => false,
|
||||||
'main_center' => ['context' => Center::class],
|
'main_center' => ['context' => Center::class],
|
||||||
'absenceStart' => ['context' => \DateTimeImmutable::class],
|
|
||||||
'absenceEnd' => ['context' => \DateTimeImmutable::class],
|
|
||||||
]];
|
]];
|
||||||
|
|
||||||
yield [null, 'docgen', ['docgen:expects' => User::class], [
|
yield [null, 'docgen', ['docgen:expects' => User::class], [
|
||||||
@@ -142,8 +138,6 @@ final class UserNormalizerTest extends TestCase
|
|||||||
'text_without_absent' => '',
|
'text_without_absent' => '',
|
||||||
'isAbsent' => false,
|
'isAbsent' => false,
|
||||||
'main_center' => ['context' => Center::class],
|
'main_center' => ['context' => Center::class],
|
||||||
'absenceStart' => null,
|
|
||||||
'absenceEnd' => null,
|
|
||||||
]];
|
]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -80,7 +80,3 @@ services:
|
|||||||
Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand:
|
Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand:
|
||||||
tags:
|
tags:
|
||||||
- {name: console.command}
|
- {name: console.command}
|
||||||
|
|
||||||
Chill\MainBundle\Command\DumpListPermissionsCommand:
|
|
||||||
autoconfigure: true
|
|
||||||
autowire: true
|
|
||||||
|
@@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\Migrations\Main;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20250722140048 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Add an absence end date for the user absence';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE users ADD absenceEnd TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
|
||||||
$this->addSql('COMMENT ON COLUMN users.absenceEnd IS \'(DC2Type:datetime_immutable)\'');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE users DROP absenceEnd');
|
|
||||||
}
|
|
||||||
}
|
|
@@ -136,7 +136,3 @@ filter_order:
|
|||||||
Search: Chercher dans la liste
|
Search: Chercher dans la liste
|
||||||
By date: Filtrer par date
|
By date: Filtrer par date
|
||||||
search_box: Filtrer par contenu
|
search_box: Filtrer par contenu
|
||||||
|
|
||||||
absence:
|
|
||||||
You are listed as absent, as of {date, date, short}: Votre absence est indiquée à partir du {date, date, short}
|
|
||||||
|
|
||||||
|
@@ -841,12 +841,12 @@ absence:
|
|||||||
# single letter for absence
|
# single letter for absence
|
||||||
A: A
|
A: A
|
||||||
My absence: Mon absence
|
My absence: Mon absence
|
||||||
Unset absence: Supprimer mes dates d'absence
|
Unset absence: Supprimer la date d'absence
|
||||||
Set absence date: Indiquer une date d'absence
|
Set absence date: Indiquer une date d'absence
|
||||||
Absence start: Absent à partir du
|
Absence start: Absent à partir du
|
||||||
Absence end: Jusqu'au
|
|
||||||
Absent: Absent
|
Absent: Absent
|
||||||
You are marked as being absent: Vous êtes indiqué absent.
|
You are marked as being absent: Vous êtes indiqué absent.
|
||||||
|
You are listed as absent, as of: Votre absence est indiquée à partir du
|
||||||
No absence listed: Aucune absence indiquée.
|
No absence listed: Aucune absence indiquée.
|
||||||
Is absent: Absent?
|
Is absent: Absent?
|
||||||
|
|
||||||
|
@@ -40,7 +40,3 @@ workflow:
|
|||||||
|
|
||||||
rolling_date:
|
rolling_date:
|
||||||
When fixed date is selected, you must provide a date: Indiquez la date fixe choisie
|
When fixed date is selected, you must provide a date: Indiquez la date fixe choisie
|
||||||
|
|
||||||
user:
|
|
||||||
absence_end_requires_start: "Vous ne pouvez pas renseigner une date de fin d'absence sans date de début."
|
|
||||||
|
|
||||||
|
@@ -78,7 +78,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
|
|||||||
* @ParamConverter("acpw1", options={"id": "acpw1_id"})
|
* @ParamConverter("acpw1", options={"id": "acpw1_id"})
|
||||||
* @ParamConverter("acpw2", options={"id": "acpw2_id"})
|
* @ParamConverter("acpw2", options={"id": "acpw2_id"})
|
||||||
*/
|
*/
|
||||||
#[Route(path: '/{_locale}/person/{acpw1_id}/acpw-duplicate/{acpw2_id}/confirm', name: 'chill_person_acpw_duplicate_confirm')]
|
#[Route(path: '/{_locale}/person/{acpw1_id}/duplicate/{acpw2_id}/confirm', name: 'chill_person_acpw_duplicate_confirm')]
|
||||||
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request)
|
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request)
|
||||||
{
|
{
|
||||||
$accompanyingPeriod = $acpw1->getAccompanyingPeriod();
|
$accompanyingPeriod = $acpw1->getAccompanyingPeriod();
|
||||||
|
Reference in New Issue
Block a user