Compare commits

..

7 Commits

26 changed files with 230 additions and 63 deletions

View File

@@ -0,0 +1,7 @@
kind: DX
body: |
Send notifications log to dedicated channel, if it exists
time: 2025-10-27T15:00:53.309372316+01:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
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

3
.changes/v4.6.1.md Normal file
View File

@@ -0,0 +1,3 @@
## v4.6.1 - 2025-10-27
### Fixed
* Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php

View File

@@ -6,6 +6,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.6.1 - 2025-10-27
### Fixed
* Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php
## v4.6.0 - 2025-10-15
### Feature
* ([#423](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/423)) Create environment banner that can be activated and configured depending on the image deployed

View File

@@ -14,7 +14,7 @@
"ext-openssl": "*",
"ext-redis": "*",
"ext-zlib": "*",
"champs-libres/wopi-bundle": "dev-master@dev",
"champs-libres/wopi-bundle": "dev-master#1be045ee95310d2037683859ecefdbf3a10f7be6 as 0.4.x-dev",
"champs-libres/wopi-lib": "dev-master@dev",
"doctrine/data-fixtures": "^1.8",
"doctrine/doctrine-bundle": "^2.1",

View File

@@ -90,7 +90,9 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt
public function getFormDefaultData(): array
{
return [];
return [
'reasons' => [],
];
}
public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array

View File

@@ -42,6 +42,8 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void
{
error_log('alterQuery called with data: '.json_encode(array_keys($data)));
// create a subquery for activity
$sqb = $qb->getEntityManager()->createQueryBuilder();
$sqb->select('1')
@@ -59,7 +61,6 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
if (\in_array('activity', $qb->getAllAliases(), true)) {
$sqb->andWhere('activity_person_having_activity.id = activity.id');
}
if (isset($data['reasons']) && [] !== $data['reasons']) {
// add clause activity reason
$sqb->join('activity_person_having_activity.reasons', 'reasons_person_having_activity');
@@ -124,12 +125,38 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
public function normalizeFormData(array $formData): array
{
return ['date_from_rolling' => $formData['date_from_rolling']->normalize(), 'date_to_rolling' => $formData['date_to_rolling']->normalize()];
$normalized = [
'date_from_rolling' => $formData['date_from_rolling']->normalize(),
'date_to_rolling' => $formData['date_to_rolling']->normalize(),
'reasons' => [],
];
if (isset($formData['reasons']) && [] !== $formData['reasons']) {
$normalized['reasons'] = array_map(
fn (ActivityReason $reason) => $reason->getId(),
$formData['reasons']
);
}
return $normalized;
}
public function denormalizeFormData(array $formData, int $fromVersion): array
{
return ['date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']), 'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling'])];
$denormalized = [
'date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']),
'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling']),
'reasons' => [],
];
if (isset($formData['reasons']) && [] !== $formData['reasons']) {
$denormalized['reasons'] = array_map(
fn ($id) => $this->activityReasonRepository->find($id),
$formData['reasons']
);
}
return $denormalized;
}
public function getFormDefaultData(): array
@@ -143,10 +170,12 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
public function describeAction($data, ExportGenerationContext $context): array
{
$reasons = $data['reasons'] ?? [];
return [
[] === $data['reasons'] ?
'export.filter.person_between_dates.describe_action_with_no_subject'
: 'export.filter.person_between_dates.describe_action_with_subject',
[] === $reasons ?
'export.filter.activity.describe_action_with_no_subject'
: 'export.filter.activity.describe_action_with_subject',
[
'date_from' => $this->rollingDateConverter->convert($data['date_from_rolling']),
'date_to' => $this->rollingDateConverter->convert($data['date_to_rolling']),
@@ -154,7 +183,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
', ',
array_map(
fn (ActivityReason $r): string => '"'.$this->translatableStringHelper->localize($r->getName()).'"',
$data['reasons']
$reasons
)
),
],
@@ -168,6 +197,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
public function validateForm($data, ExecutionContextInterface $context): void
{
error_log('validateForm called with data: '.json_encode(array_keys($data)));
if ($this->rollingDateConverter->convert($data['date_from_rolling'])
>= $this->rollingDateConverter->convert($data['date_to_rolling'])) {
$context->buildViolation('export.filter.activity.person_between_dates.date mismatch')

View File

@@ -334,7 +334,7 @@ class ChillImportUsersCommand extends Command
protected function loadUsers()
{
$reader = Reader::createFromPath($this->tempInput->getArgument('csvfile'));
$reader = Reader::from($this->tempInput->getArgument('csvfile'));
$reader->setHeaderOffset(0);
foreach ($reader->getRecords() as $line => $r) {
@@ -362,7 +362,7 @@ class ChillImportUsersCommand extends Command
protected function prepareGroupingCenters()
{
$reader = Reader::createFromPath($this->tempInput->getOption('grouping-centers'));
$reader = Reader::from($this->tempInput->getOption('grouping-centers'));
$reader->setHeaderOffset(0);
foreach ($reader->getRecords() as $r) {
@@ -378,7 +378,7 @@ class ChillImportUsersCommand extends Command
protected function prepareWriter()
{
$this->output = $output = Writer::createFromPath($this->tempInput
$this->output = $output = Writer::from($this->tempInput
->getOption('csv-dump'), 'a+');
$output->insertOne([

View File

@@ -119,7 +119,7 @@ class ChillUserSendRenewPasswordCodeCommand extends Command
protected function getReader()
{
try {
$reader = Reader::createFromPath($this->input->getArgument('csvfile'));
$reader = Reader::from($this->input->getArgument('csvfile'));
} catch (\Exception $e) {
$this->logger->error('The csv file could not be read', [
'path' => $this->input->getArgument('csvfile'),

View File

@@ -43,7 +43,7 @@ final readonly class UserExportController
$users = $this->userRepository->findAllAsArray($request->getLocale());
$csv = Writer::createFromPath('php://temp', 'r+');
$csv = Writer::from('php://temp', 'r+');
$csv->insertOne(
array_map(
fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
@@ -104,7 +104,7 @@ final readonly class UserExportController
$userPermissions = $this->userRepository->findAllUserACLAsArray();
$csv = Writer::createFromPath('php://temp', 'r+');
$csv = Writer::from('php://temp', 'r+');
$csv->insertOne(
array_map(
fn (string $e) => $this->translator->trans('admin.users.export.'.$e),

View File

@@ -64,7 +64,7 @@ class AddressReferenceBEFromBestAddress
$uncompressedStream = gzopen($tmpname, 'r');
$csv = Reader::createFromStream($uncompressedStream);
$csv = Reader::from($uncompressedStream);
$csv->setDelimiter(',');
$csv->setHeaderOffset(0);

View File

@@ -287,7 +287,7 @@ final class AddressReferenceBaseImporter
$filename = sprintf('%s-%s.csv', (new \DateTimeImmutable())->format('Ymd-His'), uniqid());
$path = Path::normalize(sprintf('%s%s%s', sys_get_temp_dir(), DIRECTORY_SEPARATOR, $filename));
$writer = Writer::createFromPath($path, 'w+');
$writer = Writer::from($path, 'w+');
// insert headers
$writer->insertOne([
'postalcode',

View File

@@ -53,7 +53,7 @@ class AddressReferenceFromBAN
// re-open it to read it
$csvDecompressed = gzopen($path, 'r');
$csv = Reader::createFromStream($csvDecompressed);
$csv = Reader::from($csvDecompressed);
$csv->setDelimiter(';')->setHeaderOffset(0);
$stmt = new Statement();
$stmt = $stmt->process($csv, [

View File

@@ -41,7 +41,7 @@ class AddressReferenceFromBano
fseek($file, 0);
$csv = Reader::createFromStream($file);
$csv = Reader::from($file);
$csv->setDelimiter(',');
$stmt = new Statement();
$stmt = $stmt->process($csv, [

View File

@@ -39,7 +39,7 @@ class AddressReferenceLU
fseek($file, 0);
$csv = Reader::createFromStream($file);
$csv = Reader::from($file);
$csv->setDelimiter(';');
$csv->setHeaderOffset(0);

View File

@@ -43,7 +43,7 @@ class PostalCodeBEFromBestAddress
$uncompressedStream = gzopen($tmpname, 'r');
$csv = Reader::createFromStream($uncompressedStream);
$csv = Reader::from($uncompressedStream);
$csv->setDelimiter(',');
$csv->setHeaderOffset(0);

View File

@@ -47,7 +47,7 @@ class PostalCodeFRFromOpenData
fseek($tmpfile, 0);
$csv = Reader::createFromStream($tmpfile);
$csv = Reader::from($tmpfile);
$csv->setDelimiter(',');
$csv->setHeaderOffset(0);

View File

@@ -18,7 +18,7 @@ use Symfony\Component\Notifier\Event\SentMessageEvent;
final readonly class SentMessageEventSubscriber implements EventSubscriberInterface
{
public function __construct(
private LoggerInterface $logger,
private LoggerInterface $notifierLogger, // will be send to "notifierLogger" if it exists
) {}
public static function getSubscribedEvents()
@@ -33,9 +33,9 @@ final readonly class SentMessageEventSubscriber implements EventSubscriberInterf
$message = $event->getMessage();
if (null === $message->getMessageId()) {
$this->logger->info('[sms] a sms message did not had any id after sending.', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId()]);
$this->notifierLogger->info('[sms] a sms message did not had any id after sending.', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId()]);
} else {
$this->logger->warning('[sms] a sms was sent', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId(), 'idsI' => $message->getMessageId()]);
$this->notifierLogger->warning('[sms] a sms was sent', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId(), 'idsI' => $message->getMessageId()]);
}
}
}

View File

@@ -127,6 +127,20 @@ 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

@@ -49,7 +49,7 @@ final class ImportSocialWorkMetadata extends Command
$filepath = $input->getOption('filepath');
try {
$csv = Reader::createFromPath($filepath);
$csv = Reader::from($filepath);
} catch (\Throwable $e) {
throw new \Exception('Error while loading CSV.', 0, $e);
}

View File

@@ -29,7 +29,7 @@ class LoadSocialWorkMetadata extends Fixture implements OrderedFixtureInterface
public function load(ObjectManager $manager): void
{
try {
$csv = Reader::createFromPath(__DIR__.'/data/social_work_metadata.csv');
$csv = Reader::from(__DIR__.'/data/social_work_metadata.csv');
} catch (\Throwable $e) {
throw new \Exception('Error while loading CSV.', 0, $e);
}

View File

@@ -60,43 +60,124 @@ import {
EVALUATION_DOCUMENT_MOVE_SUCCESS,
} from "translator";
import { useToast } from "vue-toast-notification";
import { buildLinkCreate as buildLinkCreateNotification } from "ChillMainAssets/lib/entity-notification/api";
const props = defineProps(["evaluation", "docAnchorId"]);
const store = useStore();
const $toast = useToast();
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 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 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;
@@ -193,7 +274,7 @@ function updateWarningInterval(value) {
}
function updateTimeSpent(value) {
timeSpent.value = value;
timeSpent.value = parseInt(value);
}
function updateComment(value) {

View File

@@ -216,9 +216,29 @@
{% if e.timeSpent is not null and e.timeSpent > 0 %}
<li>
{% set minutes = (e.timeSpent / 60) %}
<span
class="item-key">{{ 'accompanying_course_work.timeSpent'|trans ~ ' : ' }}</span> {{ 'duration.minute'|trans({ '{m}' : minutes }) }}
{% 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 %}
</li>
{% elseif displayContent is defined and displayContent == 'long' %}
<li>

View File

@@ -38,7 +38,7 @@ final readonly class SocialActionCSVExportService
array_keys($this->formatRow(new SocialAction()))
);
$csv = Writer::createFromPath('php://temp', 'w+');
$csv = Writer::from('php://temp', 'w+');
$csv->insertOne($headers);
foreach ($actions as $action) {

View File

@@ -36,7 +36,7 @@ readonly class SocialIssueCSVExportService
public function generateCsv(array $issues): Writer
{
// CSV headers
$csv = Writer::createFromPath('php://temp', 'r+');
$csv = Writer::from('php://temp', 'r+');
$csv->insertOne(
array_map(
fn (string $e) => $this->translator->trans($e),

View File

@@ -52,7 +52,7 @@ class ThirdpartyCSVExportController extends AbstractController
fwrite($output, "\xEF\xBB\xBF");
// Create CSV writer
$csv = Writer::createFromStream($output);
$csv = Writer::from($output);
// Write header row
$header = array_map(