Compare commits

..

1 Commits

11 changed files with 135 additions and 194 deletions

View File

@@ -0,0 +1,6 @@
kind: UX
body: Remove the label if there is only one scope and no scope picking field is displayed.
time: 2025-10-30T15:31:26.807444365+01:00
custom:
Issue: "449"
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: UX
body: Expand timeSpent choices for evaluation document and translate them to user locale or fallback 'fr'
time: 2025-10-30T18:09:19.373907522+01:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -18,6 +18,7 @@ use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\DocStoreBundle\Form\CollectionStoredObjectType;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\CommentType;
@@ -47,6 +48,7 @@ use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Security;
class ActivityType extends AbstractType
{
@@ -60,6 +62,7 @@ class ActivityType extends AbstractType
protected array $timeChoices,
protected SocialIssueRender $socialIssueRender,
protected SocialActionRender $socialActionRender,
private readonly Security $security,
) {
if (!$tokenStorage->getToken()->getUser() instanceof User) {
throw new \RuntimeException('you should have a valid user');
@@ -87,10 +90,22 @@ class ActivityType extends AbstractType
$activityType = $options['activityType'];
if (null !== $options['data']->getPerson()) {
$reachableScopes = array_values(
array_filter(
$this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
ActivityVoter::CREATE === (string) $options['role'] ? ActivityVoter::CREATE_PERSON : (string) $options['role'],
$options['center']
),
static fn (Scope $s) => $s->isActive()
)
);
$builder->add('scope', ScopePickerType::class, [
'center' => $options['center'],
'role' => ActivityVoter::CREATE === (string) $options['role'] ? ActivityVoter::CREATE_PERSON : (string) $options['role'],
'reachable_scopes' => $reachableScopes,
'required' => true,
'label' => count($reachableScopes) > 1 ? 'Scope' : false,
]);
}

View File

@@ -14,9 +14,11 @@ namespace Chill\DocStoreBundle\Form;
use Chill\DocStoreBundle\Entity\Document;
use Chill\DocStoreBundle\Entity\DocumentCategory;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
@@ -27,10 +29,11 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Security;
class PersonDocumentType extends AbstractType
{
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly ScopeResolverDispatcher $scopeResolverDispatcher, private readonly ParameterBagInterface $parameterBag, private readonly CenterResolverDispatcher $centerResolverDispatcher) {}
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly Security $security, private readonly AuthorizationHelperInterface $authorizationHelper, private readonly ScopeResolverDispatcher $scopeResolverDispatcher, private readonly ParameterBagInterface $parameterBag, private readonly CenterResolverDispatcher $centerResolverDispatcher) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
@@ -56,9 +59,19 @@ class PersonDocumentType extends AbstractType
]);
if ($isScopeConcerned && $this->parameterBag->get('chill_main')['acl']['form_show_scopes']) {
$reachableScopes = array_values(
array_filter(
$this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
$options['role'],
$this->centerResolverDispatcher->resolveCenter($document)
),
static fn (Scope $s) => $s->isActive()
)
);
$builder->add('scope', ScopePickerType::class, [
'center' => $this->centerResolverDispatcher->resolveCenter($document),
'role' => $options['role'],
'reachable_scopes' => $reachableScopes,
'label' => count($reachableScopes) > 1 ? 'Scope' : false,
]);
}
}

View File

@@ -40,41 +40,39 @@ use Symfony\Component\Security\Core\Security;
class ScopePickerType extends AbstractType
{
public function __construct(
private readonly AuthorizationHelperInterface $authorizationHelper,
private readonly Security $security,
private readonly TranslatableStringHelperInterface $translatableStringHelper,
) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$items = array_values(
array_filter(
$this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
$options['role'],
$options['center']
),
static fn (Scope $s) => $s->isActive()
)
);
/* $items = array_values(
array_filter(
$this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
$options['role'],
$options['center']
),
static fn (Scope $s) => $s->isActive()
)
);*/
if (0 === \count($items)) {
if (0 === \count($options['reachable_scopes'])) {
throw new \RuntimeException('no scopes are reachable. This form should not be shown to user');
}
if (1 !== \count($items)) {
if (1 !== \count($options['reachable_scopes'])) {
$builder->add('scope', EntityType::class, [
'class' => Scope::class,
'placeholder' => 'Choose the circle',
'choice_label' => fn (Scope $c) => $this->translatableStringHelper->localize($c->getName()),
'choices' => $items,
'choices' => $options['reachable_scopes'],
]);
$builder->setDataMapper(new ScopePickerDataMapper());
} else {
$builder->add('scope', HiddenType::class, [
'data' => $items[0]->getId(),
'data' => $options['reachable_scopes'][0]->getId(),
]);
$builder->setDataMapper(new ScopePickerDataMapper($items[0]));
$builder->setDataMapper(new ScopePickerDataMapper($options['reachable_scopes'][0]));
}
}
@@ -86,11 +84,13 @@ class ScopePickerType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$resolver
// create `center` option
->setRequired('center')
->setAllowedTypes('center', [Center::class, 'array', 'null'])
// create ``role` option
->setRequired('role')
->setAllowedTypes('role', ['string']);
->setRequired('reachable_scopes')
->setAllowedTypes('reachable_scopes', ['array']);
// create `center` option
// ->setRequired('center')
// ->setAllowedTypes('center', [Center::class, 'array', 'null'])
// create ``role` option
// ->setRequired('role')
// ->setAllowedTypes('role', ['string']);
}
}

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Form\Type;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ScopePickerType;
@@ -42,8 +41,7 @@ final class ScopePickerTypeTest extends TypeTestCase
public function estBuildOneScopeIsSuccessful()
{
$form = $this->factory->create(ScopePickerType::class, null, [
'center' => new Center(),
'role' => 'ONE_SCOPE',
'reachable_scopes' => [new Scope()],
]);
$view = $form->createView();
@@ -54,8 +52,7 @@ final class ScopePickerTypeTest extends TypeTestCase
public function testBuildThreeScopesIsSuccessful()
{
$form = $this->factory->create(ScopePickerType::class, null, [
'center' => new Center(),
'role' => 'THREE_SCOPE',
'reachable_scopes' => [new Scope(), new Scope(), new Scope()],
]);
$view = $form->createView();
@@ -66,8 +63,7 @@ final class ScopePickerTypeTest extends TypeTestCase
public function testBuildTwoScopesIsSuccessful()
{
$form = $this->factory->create(ScopePickerType::class, null, [
'center' => new Center(),
'role' => 'TWO_SCOPE',
'reachable_scopes' => [new Scope(), new Scope()],
]);
$view = $form->createView();

View File

@@ -127,20 +127,6 @@ duration:
few {# minutes}
other {# minutes}
}
hour: >-
{h, plural,
=0 {Aucune durée}
one {# heure}
few {# heures}
other {# heures}
}
day: >-
{d, plural,
=0 {Aucune durée}
one {# jour}
few {# jours}
other {# jours}
}
filter_order:
by_date:

View File

@@ -67,117 +67,37 @@ const store = useStore();
const $toast = useToast();
const timeSpentValues = [
60,
120,
180,
240,
300,
600,
900,
1200,
1500,
1800,
2700,
3600,
4500,
5400,
6300,
7200,
9000,
10800,
12600,
14400,
16200,
18000,
19800,
21600,
23400,
25200,
27000,
28800,
43200,
57600,
72000,
86400,
100800,
115200,
129600,
144000, // goes from 1 minute to 40 hours
const timeSpentChoices = [
{ text: "1 minute", value: 60 },
{ text: "2 minutes", value: 120 },
{ text: "3 minutes", value: 180 },
{ text: "4 minutes", value: 240 },
{ text: "5 minutes", value: 300 },
{ text: "10 minutes", value: 600 },
{ text: "15 minutes", value: 900 },
{ text: "20 minutes", value: 1200 },
{ text: "25 minutes", value: 1500 },
{ text: "30 minutes", value: 1800 },
{ text: "45 minutes", value: 2700 },
{ text: "1 hour", value: 3600 },
{ text: "1 hour 15 minutes", value: 4500 },
{ text: "1 hour 30 minutes", value: 5400 },
{ text: "1 hour 45 minutes", value: 6300 },
{ text: "2 hours", value: 7200 },
{ text: "2 hours 30 minutes", value: 9000 },
{ text: "3 hours", value: 10800 },
{ text: "3 hours 30 minutes", value: 12600 },
{ text: "4 hours", value: 14400 },
{ text: "4 hours 30 minutes", value: 16200 },
{ text: "5 hours", value: 18000 },
{ text: "5 hours 30 minutes", value: 19800 },
{ text: "6 hours", value: 21600 },
{ text: "6 hours 30 minutes", value: 23400 },
{ text: "7 hours", value: 25200 },
{ text: "7 hours 30 minutes", value: 27000 },
{ text: "8 hours", value: 28800 },
];
const formatDuration = (seconds, locale) => {
const currentLocale = locale || navigator.language || "fr";
const totalHours = Math.floor(seconds / 3600);
const remainingMinutes = Math.floor((seconds % 3600) / 60);
if (totalHours >= 8) {
const days = Math.floor(totalHours / 8);
const remainingHours = totalHours % 8;
const parts = [];
if (days > 0) {
parts.push(
new Intl.NumberFormat(currentLocale, {
style: "unit",
unit: "day",
unitDisplay: "long",
}).format(days),
);
}
if (remainingHours > 0) {
parts.push(
new Intl.NumberFormat(currentLocale, {
style: "unit",
unit: "hour",
unitDisplay: "long",
}).format(remainingHours),
);
}
return parts.join(" ");
}
// For less than 8 hours, use hour and minute format
const parts = [];
if (totalHours > 0) {
parts.push(
new Intl.NumberFormat(currentLocale, {
style: "unit",
unit: "hour",
unitDisplay: "long",
}).format(totalHours),
);
}
if (remainingMinutes > 0) {
parts.push(
new Intl.NumberFormat(currentLocale, {
style: "unit",
unit: "minute",
unitDisplay: "long",
}).format(remainingMinutes),
);
}
console.log(parts);
console.log(parts.join(" "));
return parts.join(" ");
};
const timeSpentChoices = computed(() => {
const locale = "fr";
return timeSpentValues.map((value) => ({
text: formatDuration(value, locale),
value: parseInt(value),
}));
});
const startDate = computed({
get() {
return props.evaluation.startDate;
@@ -274,7 +194,7 @@ function updateWarningInterval(value) {
}
function updateTimeSpent(value) {
timeSpent.value = parseInt(value);
timeSpent.value = value;
}
function updateComment(value) {

View File

@@ -216,29 +216,9 @@
{% if e.timeSpent is not null and e.timeSpent > 0 %}
<li>
{% set totalHours = (e.timeSpent / 3600)|round(0, 'floor') %}
{% set totalMinutes = ((e.timeSpent % 3600) / 60)|round(0, 'floor') %}
<span class="item-key">{{ 'accompanying_course_work.timeSpent'|trans ~ ' : ' }}</span>
{% if totalHours >= 8 %}
{% set days = (totalHours / 8)|round(0, 'floor') %}
{% set remainingHours = totalHours % 8 %}
{% if days > 0 %}
{{ 'duration.day'|trans({ '{d}' : days }) }}
{% endif %}
{% if remainingHours > 0 %}
{{ 'duration.hour'|trans({ '{h}' : remainingHours }) }}
{% endif %}
{% else %}
{% if totalHours > 0 %}
{{ 'duration.hour'|trans({ '{h}' : totalHours }) }}
{% endif %}
{% if totalMinutes > 0 %}
{{ 'duration.minute'|trans({ '{m}' : totalMinutes }) }}
{% endif %}
{% endif %}
{% set minutes = (e.timeSpent / 60) %}
<span
class="item-key">{{ 'accompanying_course_work.timeSpent'|trans ~ ' : ' }}</span> {{ 'duration.minute'|trans({ '{m}' : minutes }) }}
</li>
{% elseif displayContent is defined and displayContent == 'long' %}
<li>

View File

@@ -167,10 +167,20 @@ final readonly class PersonContext implements PersonContextInterface
}
if ($this->isScopeNecessary($entity)) {
$reachableScopes = array_values(
array_filter(
$this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
PersonDocumentVoter::CREATE,
$this->centerResolverManager->resolveCenters($entity)
),
static fn (Scope $s) => $s->isActive()
)
);
$builder->add('scope', ScopePickerType::class, [
'center' => $this->centerResolverManager->resolveCenters($entity),
'role' => PersonDocumentVoter::CREATE,
'label' => 'Scope',
'reachable_scopes' => $reachableScopes,
'label' => count($reachableScopes) > 1 ? 'Scope' : false,
]);
}
}

View File

@@ -11,11 +11,13 @@ declare(strict_types=1);
namespace Chill\TaskBundle\Form;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\DateIntervalType;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\MainBundle\Form\Type\UserPickerType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use Chill\TaskBundle\Security\Authorization\TaskVoter;
@@ -24,10 +26,18 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
class SingleTaskType extends AbstractType
{
public function __construct(private readonly ParameterBagInterface $parameterBag, private readonly CenterResolverDispatcherInterface $centerResolverDispatcher, private readonly ScopeResolverDispatcher $scopeResolverDispatcher) {}
public function __construct(
private readonly ParameterBagInterface $parameterBag,
private readonly CenterResolverDispatcherInterface $centerResolverDispatcher,
private readonly Security $security,
private readonly AuthorizationHelperInterface $authorizationHelper,
private readonly ScopeResolverDispatcher $scopeResolverDispatcher,
) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
@@ -62,11 +72,22 @@ class SingleTaskType extends AbstractType
]);
if ($isScopeConcerned && $this->parameterBag->get('chill_main')['acl']['form_show_scopes']) {
$reachableScopes = array_values(
array_filter(
$this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
$options['role'],
$center
),
static fn (Scope $s) => $s->isActive()
)
);
$builder
->add('circle', ScopePickerType::class, [
'center' => $center,
'role' => $options['role'],
'reachable_scopes' => $reachableScopes,
'required' => true,
'label' => count($reachableScopes) > 1 ? 'Scope' : false,
]);
}
}