mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge remote-tracking branch 'origin/upgrade-sf5' into signature-app-master
This commit is contained in:
commit
45323e9136
@ -1,4 +1,4 @@
|
|||||||
## v2.23.0 - 2024-07-23
|
## v2.23.0 - 2024-07-23 & 2024-07-19
|
||||||
### Feature
|
### Feature
|
||||||
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
|
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
|
||||||
* Add job bundle (module emploi)
|
* Add job bundle (module emploi)
|
||||||
@ -6,6 +6,25 @@
|
|||||||
|
|
||||||
* Upgrade CKEditor and refactor configuration with use of typescript
|
* Upgrade CKEditor and refactor configuration with use of typescript
|
||||||
|
|
||||||
|
* ([#123](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/123)) Add a button to duplicate calendar ranges from a week to another one
|
||||||
|
* [admin] filter users by active / inactive in the admin user's list
|
||||||
|
* ([#273](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/273)) Add the possibility to mark all notifications as read
|
||||||
|
|
||||||
|
|
||||||
|
* Handle duplicate reference id in the import of reference addresses
|
||||||
|
* Do not update the "createdAt" column when importing postal code which does not change
|
||||||
|
* Display filename on file upload within the UI interface
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fix resolving of centers for an household, which will fix in turn the access control
|
* Fix resolving of centers for an household, which will fix in turn the access control
|
||||||
* Resolved type hinting error in activity list export
|
* Resolved type hinting error in activity list export
|
||||||
|
* ([#271](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/271)) Take into account the acp closing date in the acp works date filter
|
||||||
|
|
||||||
|
### Traduction française des principaux changements
|
||||||
|
- Ajout d'un bouton pour dupliquer les périodes de disponibilités d'une semaine à une autre;
|
||||||
|
- dans l'interface d'administration, filtre sur les utilisateurs actifs. Par défaut, seul les utilisateurs
|
||||||
|
actifs sont affichés;
|
||||||
|
- Nouveau bouton pour indiquer toutes les notifications comme lues;
|
||||||
|
- Améliorations sur l'import des adresses et des codes postaux;
|
||||||
|
- Affiche le nom du fichier déposé quand on téléverse un fichier depuis le poste de travail local;
|
||||||
|
- Agrandit l'icône du type de fichier dans l'interface de dépôt de fichier;
|
||||||
|
- correction: tient compte de la date de fermeture du parcours dans les filtres sur les actions d'accompagnement.
|
||||||
|
3
.changes/v2.24.0.md
Normal file
3
.changes/v2.24.0.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## v2.24.0 - 2024-09-11
|
||||||
|
### Feature
|
||||||
|
* ([#306](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/306)) When a document is converted or downloaded in the browser, this document is removed from the browser memory after 45s. Future click on the button re-download the document.
|
3
.changes/v3.1.0.md
Normal file
3
.changes/v3.1.0.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## v3.1.0 - 2024-08-30
|
||||||
|
### Feature
|
||||||
|
* Add export aggregator to aggregate activities by household + filter persons that are not part of an accompanyingperiod during a certain timeframe.
|
31
CHANGELOG.md
31
CHANGELOG.md
@ -6,14 +6,43 @@ 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).
|
||||||
|
|
||||||
|
|
||||||
|
## v3.1.0 - 2024-08-30
|
||||||
|
### Feature
|
||||||
|
* Add export aggregator to aggregate activities by household + filter persons that are not part of an accompanyingperiod during a certain timeframe.
|
||||||
|
|
||||||
## v3.0.0 - 2024-08-26
|
## v3.0.0 - 2024-08-26
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fix delete action for accompanying periods in draft state
|
* Fix delete action for accompanying periods in draft state
|
||||||
* Fix connection to azure when making an calendar event in chill
|
* Fix connection to azure when making an calendar event in chill
|
||||||
* CollectionType js fixes for remove button and adding multiple entries
|
* CollectionType js fixes for remove button and adding multiple entries
|
||||||
|
|
||||||
## v2.23.0 - 2024-07-23
|
## v2.24.0 - 2024-09-11
|
||||||
### Feature
|
### Feature
|
||||||
|
* ([#306](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/306)) When a document is converted or downloaded in the browser, this document is removed from the browser memory after 45s. Future click on the button re-download the document.
|
||||||
|
|
||||||
|
## v2.23.0 - 2024-07-19 & 2024-07-23
|
||||||
|
### Feature
|
||||||
|
* ([#123](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/123)) Add a button to duplicate calendar ranges from a week to another one
|
||||||
|
* [admin] filter users by active / inactive in the admin user's list
|
||||||
|
* ([#273](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/273)) Add the possibility to mark all notifications as read
|
||||||
|
|
||||||
|
|
||||||
|
* Handle duplicate reference id in the import of reference addresses
|
||||||
|
* Do not update the "createdAt" column when importing postal code which does not change
|
||||||
|
* Display filename on file upload within the UI interface
|
||||||
|
### Fixed
|
||||||
|
* ([#271](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/271)) Take into account the acp closing date in the acp works date filter
|
||||||
|
|
||||||
|
### Traduction française des principaux changements
|
||||||
|
- Ajout d'un bouton pour dupliquer les périodes de disponibilités d'une semaine à une autre;
|
||||||
|
- dans l'interface d'administration, filtre sur les utilisateurs actifs. Par défaut, seul les utilisateurs
|
||||||
|
actifs sont affichés;
|
||||||
|
- Nouveau bouton pour indiquer toutes les notifications comme lues;
|
||||||
|
- Améliorations sur l'import des adresses et des codes postaux;
|
||||||
|
- Affiche le nom du fichier déposé quand on téléverse un fichier depuis le poste de travail local;
|
||||||
|
- Agrandit l'icône du type de fichier dans l'interface de dépôt de fichier;
|
||||||
|
- correction: tient compte de la date de fermeture du parcours dans les filtres sur les actions d'accompagnement.
|
||||||
|
|
||||||
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
|
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
|
||||||
* Add job bundle (module emploi)
|
* Add job bundle (module emploi)
|
||||||
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"symfony/dom-crawler": "^5.4",
|
"symfony/dom-crawler": "^5.4",
|
||||||
"symfony/error-handler": "^5.4",
|
"symfony/error-handler": "^5.4",
|
||||||
"symfony/event-dispatcher": "^5.4",
|
"symfony/event-dispatcher": "^5.4",
|
||||||
|
"symfony/event-dispatcher-contracts": "^2.4",
|
||||||
"symfony/expression-language": "^5.4",
|
"symfony/expression-language": "^5.4",
|
||||||
"symfony/filesystem": "^5.4",
|
"symfony/filesystem": "^5.4",
|
||||||
"symfony/finder": "^5.4",
|
"symfony/finder": "^5.4",
|
||||||
|
@ -39,9 +39,12 @@ Implements a :code:`Chill\MainBundle\Cron\CronJobInterface`. Here is an example:
|
|||||||
use Chill\MainBundle\Entity\CronJobExecution;
|
use Chill\MainBundle\Entity\CronJobExecution;
|
||||||
use DateInterval;
|
use DateInterval;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
|
|
||||||
class MyCronJob implements CronJobInterface
|
class MyCronJob implements CronJobInterface
|
||||||
{
|
{
|
||||||
|
function __construct(private ClockInterface $clock) {}
|
||||||
|
|
||||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||||
{
|
{
|
||||||
// the parameter $cronJobExecution contains data about the last execution of the cronjob
|
// the parameter $cronJobExecution contains data about the last execution of the cronjob
|
||||||
@ -56,7 +59,7 @@ Implements a :code:`Chill\MainBundle\Cron\CronJobInterface`. Here is an example:
|
|||||||
|
|
||||||
// this cron job should be executed if the last execution is greater than one day, but only during the night
|
// this cron job should be executed if the last execution is greater than one day, but only during the night
|
||||||
|
|
||||||
$now = new DateTimeImmutable('now');
|
$now = $clock->now();
|
||||||
|
|
||||||
return $cronJobExecution->getLastStart() < $now->sub(new DateInterval('P1D'))
|
return $cronJobExecution->getLastStart() < $now->sub(new DateInterval('P1D'))
|
||||||
&& in_array($now->format('H'), self::ACCEPTED_HOURS, true)
|
&& in_array($now->format('H'), self::ACCEPTED_HOURS, true)
|
||||||
@ -69,10 +72,15 @@ Implements a :code:`Chill\MainBundle\Cron\CronJobInterface`. Here is an example:
|
|||||||
return 'arbitrary-and-unique-key';
|
return 'arbitrary-and-unique-key';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(): void
|
public function run(array $lastExecutionData): void
|
||||||
{
|
{
|
||||||
// here, we execute the command
|
// here, we execute the command
|
||||||
}
|
|
||||||
|
// we return execution data, which will be served for next execution
|
||||||
|
// this data should be easily serializable in a json column: it should contains
|
||||||
|
// only int, string, etc. Avoid storing object
|
||||||
|
return ['last-execution-id' => 0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
How are cron job scheduled ?
|
How are cron job scheduled ?
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
<?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\ActivityBundle\Export\Aggregator\PersonAggregators;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Export\Declarations;
|
||||||
|
use Chill\MainBundle\Export\AggregatorInterface;
|
||||||
|
use Chill\PersonBundle\Entity\Household\Household;
|
||||||
|
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||||
|
use Chill\PersonBundle\Repository\Household\HouseholdRepository;
|
||||||
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
final readonly class HouseholdAggregator implements AggregatorInterface
|
||||||
|
{
|
||||||
|
public function __construct(private HouseholdRepository $householdRepository) {}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
|
{
|
||||||
|
// nothing to add here
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormDefaultData(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabels($key, array $values, mixed $data)
|
||||||
|
{
|
||||||
|
return function (int|string|null $value): string|int {
|
||||||
|
if ('_header' === $value) {
|
||||||
|
return 'export.aggregator.person.by_household.household';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('' === $value || null === $value || null === $household = $this->householdRepository->find($value)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $household->getId();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryKeys($data)
|
||||||
|
{
|
||||||
|
return ['activity_household_agg'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle()
|
||||||
|
{
|
||||||
|
return 'export.aggregator.person.by_household.title';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addRole(): ?string
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function alterQuery(QueryBuilder $qb, $data)
|
||||||
|
{
|
||||||
|
$qb->join(
|
||||||
|
HouseholdMember::class,
|
||||||
|
'activity_household_agg_household_member',
|
||||||
|
Join::WITH,
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('activity_household_agg_household_member.person', 'activity.person'),
|
||||||
|
$qb->expr()->lte('activity_household_agg_household_member.startDate', 'activity.date'),
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->gte('activity_household_agg_household_member.endDate', 'activity.date'),
|
||||||
|
$qb->expr()->isNull('activity_household_agg_household_member.endDate')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$qb->join(
|
||||||
|
Household::class,
|
||||||
|
'activity_household_agg_household',
|
||||||
|
Join::WITH,
|
||||||
|
$qb->expr()->eq('activity_household_agg_household_member.household', 'activity_household_agg_household')
|
||||||
|
);
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->addSelect('activity_household_agg_household.id AS activity_household_agg')
|
||||||
|
->addGroupBy('activity_household_agg');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyOn()
|
||||||
|
{
|
||||||
|
return Declarations::ACTIVITY_PERSON;
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ use Chill\MainBundle\Export\FormatterInterface;
|
|||||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||||
use Chill\MainBundle\Export\ListInterface;
|
use Chill\MainBundle\Export\ListInterface;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
|
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||||
use Doctrine\DBAL\Exception\InvalidArgumentException;
|
use Doctrine\DBAL\Exception\InvalidArgumentException;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@ -44,6 +45,7 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
|||||||
'person_firstname',
|
'person_firstname',
|
||||||
'person_lastname',
|
'person_lastname',
|
||||||
'person_id',
|
'person_id',
|
||||||
|
'household_id',
|
||||||
];
|
];
|
||||||
private readonly bool $filterStatsByCenters;
|
private readonly bool $filterStatsByCenters;
|
||||||
|
|
||||||
@ -189,19 +191,26 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
|||||||
{
|
{
|
||||||
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
||||||
|
|
||||||
// throw an error if any fields are present
|
// throw an error if no fields are present
|
||||||
if (!\array_key_exists('fields', $data)) {
|
if (!\array_key_exists('fields', $data)) {
|
||||||
throw new InvalidArgumentException('Any fields have been checked.');
|
throw new InvalidArgumentException('No fields have been checked.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$qb = $this->entityManager->createQueryBuilder();
|
$qb = $this->entityManager->createQueryBuilder();
|
||||||
|
|
||||||
$qb
|
$qb
|
||||||
->from('ChillActivityBundle:Activity', 'activity')
|
->from('ChillActivityBundle:Activity', 'activity')
|
||||||
->join('activity.person', 'actperson');
|
->join('activity.person', 'person')
|
||||||
|
->join(
|
||||||
|
HouseholdMember::class,
|
||||||
|
'householdmember',
|
||||||
|
Query\Expr\Join::WITH,
|
||||||
|
'person = householdmember.person AND householdmember.startDate <= activity.date AND (householdmember.endDate IS NULL OR householdmember.endDate > activity.date)'
|
||||||
|
)
|
||||||
|
->join('householdmember.household', 'household');
|
||||||
|
|
||||||
if ($this->filterStatsByCenters) {
|
if ($this->filterStatsByCenters) {
|
||||||
$qb->join('actperson.centerHistory', 'centerHistory');
|
$qb->join('person.centerHistory', 'centerHistory');
|
||||||
$qb->where(
|
$qb->where(
|
||||||
$qb->expr()->andX(
|
$qb->expr()->andX(
|
||||||
$qb->expr()->lte('centerHistory.startDate', 'activity.date'),
|
$qb->expr()->lte('centerHistory.startDate', 'activity.date'),
|
||||||
@ -224,17 +233,22 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'person_firstname':
|
case 'person_firstname':
|
||||||
$qb->addSelect('actperson.firstName AS person_firstname');
|
$qb->addSelect('person.firstName AS person_firstname');
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'person_lastname':
|
case 'person_lastname':
|
||||||
$qb->addSelect('actperson.lastName AS person_lastname');
|
$qb->addSelect('person.lastName AS person_lastname');
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'person_id':
|
case 'person_id':
|
||||||
$qb->addSelect('actperson.id AS person_id');
|
$qb->addSelect('person.id AS person_id');
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'household_id':
|
||||||
|
$qb->addSelect('household.id AS household_id');
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -284,7 +298,7 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
|||||||
return ActivityStatsVoter::LISTS;
|
return ActivityStatsVoter::LISTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supportsModifiers()
|
public function supportsModifiers(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Declarations::ACTIVITY,
|
Declarations::ACTIVITY,
|
||||||
|
@ -73,7 +73,7 @@ final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInt
|
|||||||
|
|
||||||
$qb->andWhere(
|
$qb->andWhere(
|
||||||
$qb->expr()->exists(
|
$qb->expr()->exists(
|
||||||
'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = activity.accompanyingPeriod"
|
'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = acp"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function alterQuery(QueryBuilder $qb, $data)
|
public function alterQuery(QueryBuilder $qb, $data): void
|
||||||
{
|
{
|
||||||
// create a subquery for activity
|
// create a subquery for activity
|
||||||
$sqb = $qb->getEntityManager()->createQueryBuilder();
|
$sqb = $qb->getEntityManager()->createQueryBuilder();
|
||||||
@ -121,7 +121,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeAction($data, $format = 'string')
|
public function describeAction($data, $format = 'string'): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
[] === $data['reasons'] ?
|
[] === $data['reasons'] ?
|
||||||
@ -141,7 +141,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTitle()
|
public function getTitle(): string
|
||||||
{
|
{
|
||||||
return 'export.filter.activity.person_between_dates.title';
|
return 'export.filter.activity.person_between_dates.title';
|
||||||
}
|
}
|
||||||
|
@ -243,3 +243,7 @@ services:
|
|||||||
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\PersonAggregator:
|
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\PersonAggregator:
|
||||||
tags:
|
tags:
|
||||||
- { name: chill.export_aggregator, alias: activity_person_agg }
|
- { name: chill.export_aggregator, alias: activity_person_agg }
|
||||||
|
|
||||||
|
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\HouseholdAggregator:
|
||||||
|
tags:
|
||||||
|
- { name: chill.export_aggregator, alias: activity_household_agg }
|
||||||
|
@ -428,6 +428,9 @@ export:
|
|||||||
by_person:
|
by_person:
|
||||||
title: Grouper les échanges par usager (dossier d'usager dans lequel l'échange est enregistré)
|
title: Grouper les échanges par usager (dossier d'usager dans lequel l'échange est enregistré)
|
||||||
person: Usager
|
person: Usager
|
||||||
|
by_household:
|
||||||
|
title: Grouper les échanges par ménage
|
||||||
|
household: Identifiant ménage
|
||||||
acp:
|
acp:
|
||||||
by_activity_type:
|
by_activity_type:
|
||||||
title: Grouper les parcours par type d'échange
|
title: Grouper les parcours par type d'échange
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<label class="form-label">{{ $t('created_availabilities') }}</label>
|
<label class="form-label">{{ $t("created_availabilities") }}</label>
|
||||||
<vue-multiselect
|
<vue-multiselect
|
||||||
v-model="pickedLocation"
|
v-model="pickedLocation"
|
||||||
:options="locations"
|
:options="locations"
|
||||||
@ -14,10 +14,15 @@
|
|||||||
></vue-multiselect>
|
></vue-multiselect>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-options row justify-content-between" style="margin-top: 1rem;">
|
<div
|
||||||
|
class="display-options row justify-content-between"
|
||||||
|
style="margin-top: 1rem"
|
||||||
|
>
|
||||||
<div class="col-sm-9 col-xs-12">
|
<div class="col-sm-9 col-xs-12">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<label class="input-group-text" for="slotDuration">Durée des créneaux</label>
|
<label class="input-group-text" for="slotDuration"
|
||||||
|
>Durée des créneaux</label
|
||||||
|
>
|
||||||
<select v-model="slotDuration" id="slotDuration" class="form-select">
|
<select v-model="slotDuration" id="slotDuration" class="form-select">
|
||||||
<option value="00:05:00">5 minutes</option>
|
<option value="00:05:00">5 minutes</option>
|
||||||
<option value="00:10:00">10 minutes</option>
|
<option value="00:10:00">10 minutes</option>
|
||||||
@ -58,13 +63,20 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-3 col-xs-12">
|
<div class="col-xs-12 col-sm-3">
|
||||||
<div class="float-end">
|
<div class="float-end">
|
||||||
<div class="form-check input-group">
|
<div class="form-check input-group">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<input id="showHideWE" class="mt-0" type="checkbox" v-model="showWeekends">
|
<input
|
||||||
|
id="showHideWE"
|
||||||
|
class="mt-0"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="showWeekends"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<label for="showHideWE" class="form-check-label input-group-text">Week-ends</label>
|
<label for="showHideWE" class="form-check-label input-group-text"
|
||||||
|
>Week-ends</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -72,39 +84,86 @@
|
|||||||
<FullCalendar :options="calendarOptions" ref="calendarRef">
|
<FullCalendar :options="calendarOptions" ref="calendarRef">
|
||||||
<template v-slot:eventContent="arg: EventApi">
|
<template v-slot:eventContent="arg: EventApi">
|
||||||
<span :class="eventClasses(arg.event)">
|
<span :class="eventClasses(arg.event)">
|
||||||
<b v-if="arg.event.extendedProps.is === 'remote'">{{ arg.event.title}}</b>
|
<b v-if="arg.event.extendedProps.is === 'remote'">{{
|
||||||
<b v-else-if="arg.event.extendedProps.is === 'range'">{{ arg.timeText }} - {{ arg.event.extendedProps.locationName }}</b>
|
arg.event.title
|
||||||
<b v-else-if="arg.event.extendedProps.is === 'local'">{{ arg.event.title}}</b>
|
}}</b>
|
||||||
<b v-else >no 'is'</b>
|
<b v-else-if="arg.event.extendedProps.is === 'range'"
|
||||||
<a v-if="arg.event.extendedProps.is === 'range'" class="fa fa-fw fa-times delete"
|
>{{ arg.timeText }} - {{ arg.event.extendedProps.locationName }}</b
|
||||||
@click.prevent="onClickDelete(arg.event)">
|
>
|
||||||
</a>
|
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
|
||||||
</span>
|
arg.event.title
|
||||||
|
}}</b>
|
||||||
|
<b v-else>no 'is'</b>
|
||||||
|
<a
|
||||||
|
v-if="arg.event.extendedProps.is === 'range'"
|
||||||
|
class="fa fa-fw fa-times delete"
|
||||||
|
@click.prevent="onClickDelete(arg.event)"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</FullCalendar>
|
</FullCalendar>
|
||||||
|
|
||||||
<div id="copy-widget">
|
<div id="copy-widget">
|
||||||
<div class="container">
|
<div class="container mt-2 mb-2">
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col-sm-4 col-xs-12">
|
<div class="row justify-content-between align-items-center mb-4">
|
||||||
<h6 class="chill-red">{{ $t('copy_range_from_to') }}</h6>
|
<div class="col-xs-12 col-sm-3 col-md-2">
|
||||||
</div>
|
<h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6>
|
||||||
<div class="col-sm-3 col-xs-12">
|
</div>
|
||||||
<input class="form-control" type="date" v-model="copyFrom" />
|
<div class="col-xs-12 col-sm-9 col-md-2">
|
||||||
</div>
|
<select v-model="dayOrWeek" id="dayOrWeek" class="form-select">
|
||||||
<div class="col-sm-1 col-xs-12" style="text-align: center; font-size: x-large;">
|
<option value="day">{{ $t("from_day_to_day") }}</option>
|
||||||
<i class="fa fa-angle-double-right"></i>
|
<option value="week">{{ $t("from_week_to_week") }}</option>
|
||||||
</div>
|
</select>
|
||||||
<div class="col-sm-3 col-xs-12" >
|
</div>
|
||||||
<input class="form-control" type="date" v-model="copyTo" />
|
<template v-if="dayOrWeek === 'day'">
|
||||||
</div>
|
<div class="col-xs-12 col-sm-3 col-md-3">
|
||||||
<div class="col-sm-1">
|
<input class="form-control" type="date" v-model="copyFrom" />
|
||||||
<button class="btn btn-action" @click="copyDay">
|
</div>
|
||||||
{{ $t('copy_range') }}
|
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron">
|
||||||
</button>
|
<i class="fa fa-angle-double-right"></i>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-sm-3 col-md-3">
|
||||||
|
<input class="form-control" type="date" v-model="copyTo" />
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-sm-5 col-md-1">
|
||||||
|
<button class="btn btn-action float-end" @click="copyDay">
|
||||||
|
{{ $t("copy_range") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="col-xs-12 col-sm-3 col-md-3">
|
||||||
|
<select
|
||||||
|
v-model="copyFromWeek"
|
||||||
|
id="copyFromWeek"
|
||||||
|
class="form-select"
|
||||||
|
>
|
||||||
|
<option v-for="w in lastWeeks" :value="w.value" :key="w.value">
|
||||||
|
{{ w.text }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron">
|
||||||
|
<i class="fa fa-angle-double-right"></i>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-sm-3 col-md-3">
|
||||||
|
<select v-model="copyToWeek" id="copyToWeek" class="form-select">
|
||||||
|
<option v-for="w in nextWeeks" :value="w.value" :key="w.value">
|
||||||
|
{{ w.text }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-sm-5 col-md-1">
|
||||||
|
<button class="btn btn-action float-end" @click="copyWeek">
|
||||||
|
{{ $t("copy_range") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- not directly seen, but include in a modal -->
|
<!-- not directly seen, but include in a modal -->
|
||||||
@ -112,42 +171,95 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CalendarOptions,
|
CalendarOptions,
|
||||||
DatesSetArg,
|
DatesSetArg,
|
||||||
EventInput
|
EventInput,
|
||||||
} from '@fullcalendar/core';
|
} from "@fullcalendar/core";
|
||||||
import {reactive, computed, ref} from "vue";
|
import { reactive, computed, ref, onMounted } from "vue";
|
||||||
import {useStore} from "vuex";
|
import { useStore } from "vuex";
|
||||||
import {key} from './store';
|
import { key } from "./store";
|
||||||
import FullCalendar from '@fullcalendar/vue3';
|
import FullCalendar from "@fullcalendar/vue3";
|
||||||
import frLocale from '@fullcalendar/core/locales/fr';
|
import frLocale from "@fullcalendar/core/locales/fr";
|
||||||
import interactionPlugin, {DropArg, EventResizeDoneArg} from "@fullcalendar/interaction";
|
import interactionPlugin, {
|
||||||
|
DropArg,
|
||||||
|
EventResizeDoneArg,
|
||||||
|
} from "@fullcalendar/interaction";
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import {EventApi, DateSelectArg, EventDropArg, EventClickArg} from "@fullcalendar/core";
|
import {
|
||||||
import {ISOToDate} from "../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
EventApi,
|
||||||
|
DateSelectArg,
|
||||||
|
EventDropArg,
|
||||||
|
EventClickArg,
|
||||||
|
} from "@fullcalendar/core";
|
||||||
|
import {
|
||||||
|
dateToISO,
|
||||||
|
ISOToDate,
|
||||||
|
} from "../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||||
import VueMultiselect from "vue-multiselect";
|
import VueMultiselect from "vue-multiselect";
|
||||||
import {Location} from "../../../../../ChillMainBundle/Resources/public/types";
|
import { Location } from "../../../../../ChillMainBundle/Resources/public/types";
|
||||||
import EditLocation from "./Components/EditLocation.vue";
|
import EditLocation from "./Components/EditLocation.vue";
|
||||||
import {useI18n} from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const store = useStore(key);
|
const store = useStore(key);
|
||||||
|
|
||||||
const {t} = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const showWeekends = ref(false);
|
const showWeekends = ref(false);
|
||||||
const slotDuration = ref('00:05:00');
|
const slotDuration = ref("00:15:00");
|
||||||
const slotMinTime = ref('09:00:00');
|
const slotMinTime = ref("09:00:00");
|
||||||
const slotMaxTime = ref('18:00:00');
|
const slotMaxTime = ref("18:00:00");
|
||||||
const copyFrom = ref<string | null>(null);
|
const copyFrom = ref<string | null>(null);
|
||||||
const copyTo = ref<string | null>(null);
|
const copyTo = ref<string | null>(null);
|
||||||
const editLocation = ref<InstanceType<typeof EditLocation> | null>(null)
|
const editLocation = ref<InstanceType<typeof EditLocation> | null>(null);
|
||||||
|
const dayOrWeek = ref("day");
|
||||||
|
const copyFromWeek = ref<string | null>(null);
|
||||||
|
const copyToWeek = ref<string | null>(null);
|
||||||
|
|
||||||
|
interface Weeks {
|
||||||
|
value: string | null;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMonday = (week: number): Date => {
|
||||||
|
const lastMonday = new Date();
|
||||||
|
lastMonday.setDate(
|
||||||
|
lastMonday.getDate() - ((lastMonday.getDay() + 6) % 7) + week * 7
|
||||||
|
);
|
||||||
|
return lastMonday;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateOptions: Intl.DateTimeFormatOptions = {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastWeeks = computed((): Weeks[] =>
|
||||||
|
Array.from(Array(30).keys()).map((w) => {
|
||||||
|
const lastMonday = getMonday(15-w);
|
||||||
|
return {
|
||||||
|
value: dateToISO(lastMonday),
|
||||||
|
text: `Semaine du ${lastMonday.toLocaleDateString("fr-FR", dateOptions)}`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextWeeks = computed((): Weeks[] =>
|
||||||
|
Array.from(Array(52).keys()).map((w) => {
|
||||||
|
const nextMonday = getMonday(w + 1);
|
||||||
|
return {
|
||||||
|
value: dateToISO(nextMonday),
|
||||||
|
text: `Semaine du ${nextMonday.toLocaleDateString("fr-FR", dateOptions)}`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const baseOptions = ref<CalendarOptions>({
|
const baseOptions = ref<CalendarOptions>({
|
||||||
locale: frLocale,
|
locale: frLocale,
|
||||||
plugins: [interactionPlugin, timeGridPlugin],
|
plugins: [interactionPlugin, timeGridPlugin],
|
||||||
initialView: 'timeGridWeek',
|
initialView: "timeGridWeek",
|
||||||
initialDate: new Date(),
|
initialDate: new Date(),
|
||||||
scrollTimeReset: false,
|
scrollTimeReset: false,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
@ -164,9 +276,9 @@ const baseOptions = ref<CalendarOptions>({
|
|||||||
selectMirror: false,
|
selectMirror: false,
|
||||||
editable: true,
|
editable: true,
|
||||||
headerToolbar: {
|
headerToolbar: {
|
||||||
left: 'prev,next today',
|
left: "prev,next today",
|
||||||
center: 'title',
|
center: "title",
|
||||||
right: 'timeGridWeek,timeGridDay'
|
right: "timeGridWeek,timeGridDay",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -180,20 +292,23 @@ const locations = computed<Location[]>(() => {
|
|||||||
|
|
||||||
const pickedLocation = computed<Location | null>({
|
const pickedLocation = computed<Location | null>({
|
||||||
get(): Location | null {
|
get(): Location | null {
|
||||||
return store.state.locations.locationPicked || store.state.locations.currentLocation;
|
return (
|
||||||
|
store.state.locations.locationPicked ||
|
||||||
|
store.state.locations.currentLocation
|
||||||
|
);
|
||||||
},
|
},
|
||||||
set(newLocation: Location | null): void {
|
set(newLocation: Location | null): void {
|
||||||
store.commit('locations/setLocationPicked', newLocation, {root: true});
|
store.commit("locations/setLocationPicked", newLocation, { root: true });
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return the show classes for the event
|
* return the show classes for the event
|
||||||
* @param arg
|
* @param arg
|
||||||
*/
|
*/
|
||||||
const eventClasses = function(arg: EventApi): object {
|
const eventClasses = function (arg: EventApi): object {
|
||||||
return {'calendarRangeItems': true};
|
return { calendarRangeItems: true };
|
||||||
}
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// currently, all events are stored into calendarRanges, due to reactivity bug
|
// currently, all events are stored into calendarRanges, due to reactivity bug
|
||||||
@ -230,51 +345,60 @@ const calendarOptions = computed((): CalendarOptions => {
|
|||||||
* launched when the calendar range date change
|
* launched when the calendar range date change
|
||||||
*/
|
*/
|
||||||
function onDatesSet(event: DatesSetArg): void {
|
function onDatesSet(event: DatesSetArg): void {
|
||||||
store.dispatch('fullCalendar/setCurrentDatesView', {start: event.start, end: event.end});
|
store.dispatch("fullCalendar/setCurrentDatesView", {
|
||||||
|
start: event.start,
|
||||||
|
end: event.end,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDateSelect(event: DateSelectArg): void {
|
function onDateSelect(event: DateSelectArg): void {
|
||||||
|
|
||||||
if (null === pickedLocation.value) {
|
if (null === pickedLocation.value) {
|
||||||
window.alert("Indiquez une localisation avant de créer une période de disponibilité.");
|
window.alert(
|
||||||
|
"Indiquez une localisation avant de créer une période de disponibilité."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
store.dispatch('calendarRanges/createRange', {start: event.start, end: event.end, location: pickedLocation.value});
|
store.dispatch("calendarRanges/createRange", {
|
||||||
|
start: event.start,
|
||||||
|
end: event.end,
|
||||||
|
location: pickedLocation.value,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When a calendar range is deleted
|
* When a calendar range is deleted
|
||||||
*/
|
*/
|
||||||
function onClickDelete(event: EventApi): void {
|
function onClickDelete(event: EventApi): void {
|
||||||
console.log('onClickDelete', event);
|
if (event.extendedProps.is !== "range") {
|
||||||
|
|
||||||
if (event.extendedProps.is !== 'range') {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
store.dispatch('calendarRanges/deleteRange', event.extendedProps.calendarRangeId);
|
store.dispatch(
|
||||||
|
"calendarRanges/deleteRange",
|
||||||
|
event.extendedProps.calendarRangeId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEventDropOrResize(payload: EventDropArg | EventResizeDoneArg) {
|
function onEventDropOrResize(payload: EventDropArg | EventResizeDoneArg) {
|
||||||
if (payload.event.extendedProps.is !== 'range') {
|
if (payload.event.extendedProps.is !== "range") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const changedEvent = payload.event;
|
const changedEvent = payload.event;
|
||||||
|
|
||||||
store.dispatch('calendarRanges/patchRangeTime', {
|
store.dispatch("calendarRanges/patchRangeTime", {
|
||||||
calendarRangeId: payload.event.extendedProps.calendarRangeId,
|
calendarRangeId: payload.event.extendedProps.calendarRangeId,
|
||||||
start: payload.event.start,
|
start: payload.event.start,
|
||||||
end: payload.event.end,
|
end: payload.event.end,
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
function onEventClick(payload: EventClickArg): void {
|
function onEventClick(payload: EventClickArg): void {
|
||||||
// @ts-ignore TS does not recognize the target. But it does exists.
|
// @ts-ignore TS does not recognize the target. But it does exists.
|
||||||
if (payload.jsEvent.target.classList.contains('delete')) {
|
if (payload.jsEvent.target.classList.contains("delete")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (payload.event.extendedProps.is !== 'range') {
|
if (payload.event.extendedProps.is !== "range") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,10 +409,26 @@ function copyDay() {
|
|||||||
if (null === copyFrom.value || null === copyTo.value) {
|
if (null === copyFrom.value || null === copyTo.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
store.dispatch("calendarRanges/copyFromDayToAnotherDay", {
|
||||||
store.dispatch('calendarRanges/copyFromDayToAnotherDay', {from: ISOToDate(copyFrom.value), to: ISOToDate(copyTo.value)})
|
from: ISOToDate(copyFrom.value),
|
||||||
|
to: ISOToDate(copyTo.value),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyWeek() {
|
||||||
|
if (null === copyFromWeek.value || null === copyToWeek.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.dispatch("calendarRanges/copyFromWeekToAnotherWeek", {
|
||||||
|
fromMonday: ISOToDate(copyFromWeek.value),
|
||||||
|
toMonday: ISOToDate(copyToWeek.value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
copyFromWeek.value = dateToISO(getMonday(0));
|
||||||
|
copyToWeek.value = dateToISO(getMonday(1));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -299,4 +439,9 @@ function copyDay() {
|
|||||||
z-index: 9999999999;
|
z-index: 9999999999;
|
||||||
padding: 0.25rem 0 0.25rem;
|
padding: 0.25rem 0 0.25rem;
|
||||||
}
|
}
|
||||||
|
div.copy-chevron {
|
||||||
|
text-align: center;
|
||||||
|
font-size: x-large;
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -5,11 +5,9 @@ const appMessages = {
|
|||||||
show_my_calendar: "Afficher mon calendrier",
|
show_my_calendar: "Afficher mon calendrier",
|
||||||
show_weekends: "Afficher les week-ends",
|
show_weekends: "Afficher les week-ends",
|
||||||
copy_range: "Copier",
|
copy_range: "Copier",
|
||||||
copy_range_from_to: "Copier les plages d'un jour à l'autre",
|
copy_range_from_to: "Copier les plages",
|
||||||
copy_range_to_next_day: "Copier les plages du jour au jour suivant",
|
from_day_to_day: "d'un jour à l'autre",
|
||||||
copy_range_from_day: "Copier les plages du ",
|
from_week_to_week: "d'une semaine à l'autre",
|
||||||
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.",
|
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",
|
new_range_to_save: "Nouvelles plages à enregistrer",
|
||||||
update_range_to_save: "Plages à modifier",
|
update_range_to_save: "Plages à modifier",
|
||||||
|
@ -52,6 +52,23 @@ export default <Module<CalendarRangesState, State>>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return founds;
|
||||||
|
},
|
||||||
|
getRangesOnWeek: (state: CalendarRangesState) => (mondayDate: Date): EventInputCalendarRange[] => {
|
||||||
|
const founds = [];
|
||||||
|
for (let d of Array.from(Array(7).keys())) {
|
||||||
|
const dateOfWeek = new Date(mondayDate);
|
||||||
|
dateOfWeek.setDate(mondayDate.getDate() + d);
|
||||||
|
const dateStr = <string>dateToISO(dateOfWeek);
|
||||||
|
for (let range of state.ranges) {
|
||||||
|
if (isEventInputCalendarRange(range)
|
||||||
|
&& range.start.startsWith(dateStr)
|
||||||
|
) {
|
||||||
|
founds.push(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return founds;
|
return founds;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -238,7 +255,7 @@ export default <Module<CalendarRangesState, State>>{
|
|||||||
|
|
||||||
for (let r of rangesToCopy) {
|
for (let r of rangesToCopy) {
|
||||||
let start = new Date(<Date>ISOToDatetime(r.start));
|
let start = new Date(<Date>ISOToDatetime(r.start));
|
||||||
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate())
|
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||||
let end = new Date(<Date>ISOToDatetime(r.end));
|
let end = new Date(<Date>ISOToDatetime(r.end));
|
||||||
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||||
let location = ctx.rootGetters['locations/getLocationById'](r.locationId);
|
let location = ctx.rootGetters['locations/getLocationById'](r.locationId);
|
||||||
@ -246,6 +263,23 @@ export default <Module<CalendarRangesState, State>>{
|
|||||||
promises.push(ctx.dispatch('createRange', {start, end, location}));
|
promises.push(ctx.dispatch('createRange', {start, end, location}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises).then(_ => Promise.resolve(null));
|
||||||
|
},
|
||||||
|
copyFromWeekToAnotherWeek(ctx, {fromMonday, toMonday}: {fromMonday: Date, toMonday: Date}): Promise<null> {
|
||||||
|
|
||||||
|
const rangesToCopy: EventInputCalendarRange[] = ctx.getters['getRangesOnWeek'](fromMonday);
|
||||||
|
const promises = [];
|
||||||
|
const diffTime = toMonday.getTime() - fromMonday.getTime();
|
||||||
|
for (let r of rangesToCopy) {
|
||||||
|
let start = new Date(<Date>ISOToDatetime(r.start));
|
||||||
|
let end = new Date(<Date>ISOToDatetime(r.end));
|
||||||
|
start.setTime(start.getTime() + diffTime);
|
||||||
|
end.setTime(end.getTime() + diffTime);
|
||||||
|
let location = ctx.rootGetters['locations/getLocationById'](r.locationId);
|
||||||
|
|
||||||
|
promises.push(ctx.dispatch('createRange', {start, end, location}));
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.all(promises).then(_ => Promise.resolve(null));
|
return Promise.all(promises).then(_ => Promise.resolve(null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const is_dragging: Ref<boolean> = ref(false);
|
const is_dragging: Ref<boolean> = ref(false);
|
||||||
const uploading: Ref<boolean> = ref(false);
|
const uploading: Ref<boolean> = ref(false);
|
||||||
|
const display_filename: Ref<string|null> = ref(null);
|
||||||
|
|
||||||
const has_existing_doc = computed<boolean>(() => {
|
const has_existing_doc = computed<boolean>(() => {
|
||||||
return props.existingDoc !== undefined && props.existingDoc !== null;
|
return props.existingDoc !== undefined && props.existingDoc !== null;
|
||||||
@ -77,6 +78,7 @@ const onFileChange = async (event: Event): Promise<void> => {
|
|||||||
|
|
||||||
const handleFile = async (file: File): Promise<void> => {
|
const handleFile = async (file: File): Promise<void> => {
|
||||||
uploading.value = true;
|
uploading.value = true;
|
||||||
|
display_filename.value = file.name;
|
||||||
const type = file.type;
|
const type = file.type;
|
||||||
|
|
||||||
// create a stored_object if not exists
|
// create a stored_object if not exists
|
||||||
@ -108,7 +110,7 @@ const handleFile = async (file: File): Promise<void> => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="drop-file">
|
<div class="drop-file">
|
||||||
<div v-if="!uploading" :class="{ area: true, dragging: is_dragging}" @click="onZoneClick" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
<div v-if="!uploading" :class="{ area: true, dragging: is_dragging}" @click="onZoneClick" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||||
<p v-if="has_existing_doc">
|
<p v-if="has_existing_doc" class="file-icon">
|
||||||
<i class="fa fa-file-pdf-o" v-if="props.existingDoc?.type === 'application/pdf'"></i>
|
<i class="fa fa-file-pdf-o" v-if="props.existingDoc?.type === 'application/pdf'"></i>
|
||||||
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.oasis.opendocument.text'"></i>
|
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.oasis.opendocument.text'"></i>
|
||||||
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'"></i>
|
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'"></i>
|
||||||
@ -120,6 +122,8 @@ const handleFile = async (file: File): Promise<void> => {
|
|||||||
<i class="fa fa-file-archive-o" v-else-if="props.existingDoc?.type === 'application/x-zip-compressed'"></i>
|
<i class="fa fa-file-archive-o" v-else-if="props.existingDoc?.type === 'application/x-zip-compressed'"></i>
|
||||||
<i class="fa fa-file-code-o" v-else ></i>
|
<i class="fa fa-file-code-o" v-else ></i>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p v-if="display_filename !== null" class="display-filename">{{ display_filename }}</p>
|
||||||
<!-- todo i18n -->
|
<!-- todo i18n -->
|
||||||
<p v-if="has_existing_doc">Déposez un document ou cliquez ici pour remplacer le document existant</p>
|
<p v-if="has_existing_doc">Déposez un document ou cliquez ici pour remplacer le document existant</p>
|
||||||
<p v-else>Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier</p>
|
<p v-else>Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier</p>
|
||||||
@ -135,9 +139,18 @@ const handleFile = async (file: File): Promise<void> => {
|
|||||||
.drop-file {
|
.drop-file {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: xx-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-filename {
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
& > .area, & > .waiting {
|
& > .area, & > .waiting {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 8rem;
|
height: 10rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -158,4 +171,5 @@ const handleFile = async (file: File): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<a :class="props.classes" @click="download_and_open($event)">
|
<a :class="props.classes" @click="download_and_open($event)" ref="btn">
|
||||||
<i class="fa fa-file-pdf-o"></i>
|
<i class="fa fa-file-pdf-o"></i>
|
||||||
Télécharger en pdf
|
Télécharger en pdf
|
||||||
</a>
|
</a>
|
||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import {build_convert_link, download_and_decrypt_doc, download_doc} from "./helpers";
|
import {build_convert_link, download_and_decrypt_doc, download_doc} from "./helpers";
|
||||||
import mime from "mime";
|
import mime from "mime";
|
||||||
import {reactive} from "vue";
|
import {reactive, ref} from "vue";
|
||||||
import {StoredObject} from "../../types";
|
import {StoredObject} from "../../types";
|
||||||
|
|
||||||
interface ConvertButtonConfig {
|
interface ConvertButtonConfig {
|
||||||
@ -24,6 +24,7 @@ interface DownloadButtonState {
|
|||||||
|
|
||||||
const props = defineProps<ConvertButtonConfig>();
|
const props = defineProps<ConvertButtonConfig>();
|
||||||
const state: DownloadButtonState = reactive({content: null});
|
const state: DownloadButtonState = reactive({content: null});
|
||||||
|
const btn = ref<HTMLAnchorElement | null>(null);
|
||||||
|
|
||||||
async function download_and_open(event: Event): Promise<void> {
|
async function download_and_open(event: Event): Promise<void> {
|
||||||
const button = event.target as HTMLAnchorElement;
|
const button = event.target as HTMLAnchorElement;
|
||||||
@ -41,6 +42,14 @@ async function download_and_open(event: Event): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.click();
|
button.click();
|
||||||
|
const reset_pending = setTimeout(reset_state, 45000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset_state(): void {
|
||||||
|
state.content = null;
|
||||||
|
btn.value?.removeAttribute('download');
|
||||||
|
btn.value?.removeAttribute('href');
|
||||||
|
btn.value?.removeAttribute('type');
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -76,6 +76,15 @@ async function download_and_open(event: Event): Promise<void> {
|
|||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
open_button.value?.click();
|
open_button.value?.click();
|
||||||
|
console.log('open button should have been clicked');
|
||||||
|
|
||||||
|
const timer = setTimeout(reset_state, 45000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset_state(): void {
|
||||||
|
state.href_url = '#';
|
||||||
|
state.is_ready = false;
|
||||||
|
state.is_running = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -94,4 +94,38 @@ class NotificationApiController
|
|||||||
|
|
||||||
return new JsonResponse(null, JsonResponse::HTTP_ACCEPTED, [], false);
|
return new JsonResponse(null, JsonResponse::HTTP_ACCEPTED, [], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/mark/allread", name="chill_api_main_notification_mark_allread", methods={"POST"})
|
||||||
|
*/
|
||||||
|
public function markAllRead(): JsonResponse
|
||||||
|
{
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw new \RuntimeException('Invalid user');
|
||||||
|
}
|
||||||
|
|
||||||
|
$modifiedNotificationIds = $this->notificationRepository->markAllNotificationAsReadForUser($user);
|
||||||
|
|
||||||
|
return new JsonResponse($modifiedNotificationIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/mark/undoallread", name="chill_api_main_notification_mark_undoallread", methods={"POST"})
|
||||||
|
*/
|
||||||
|
public function undoAllRead(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw new \RuntimeException('Invalid user');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
$touchedIds = $this->notificationRepository->markAllNotificationAsUnreadForUser($user, $ids);
|
||||||
|
|
||||||
|
return new JsonResponse($touchedIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,7 @@ class NotificationController extends AbstractController
|
|||||||
#[Route(path: '/inbox', name: 'chill_main_notification_my')]
|
#[Route(path: '/inbox', name: 'chill_main_notification_my')]
|
||||||
public function inboxAction(): Response
|
public function inboxAction(): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||||
$currentUser = $this->security->getUser();
|
$currentUser = $this->security->getUser();
|
||||||
|
|
||||||
$notificationsNbr = $this->notificationRepository->countAllForAttendee($currentUser);
|
$notificationsNbr = $this->notificationRepository->countAllForAttendee($currentUser);
|
||||||
@ -177,8 +177,8 @@ class NotificationController extends AbstractController
|
|||||||
|
|
||||||
$notifications = $this->notificationRepository->findAllForAttendee(
|
$notifications = $this->notificationRepository->findAllForAttendee(
|
||||||
$currentUser,
|
$currentUser,
|
||||||
$limit = $paginator->getItemsPerPage(),
|
$paginator->getItemsPerPage(),
|
||||||
$offset = $paginator->getCurrentPage()->getFirstItemNumber()
|
$paginator->getCurrentPage()->getFirstItemNumber()
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->render('@ChillMain/Notification/list.html.twig', [
|
return $this->render('@ChillMain/Notification/list.html.twig', [
|
||||||
|
@ -278,7 +278,7 @@ final class PasswordController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return \Symfony\Component\Form\Form
|
* @return \Symfony\Component\Form\FormInterface
|
||||||
*/
|
*/
|
||||||
private function passwordForm(User $user)
|
private function passwordForm(User $user)
|
||||||
{
|
{
|
||||||
|
@ -214,7 +214,7 @@ class UserController extends CRUDController
|
|||||||
|
|
||||||
return $this->redirect(
|
return $this->redirect(
|
||||||
$request->query->has('returnPath') ? $request->query->get('returnPath') :
|
$request->query->has('returnPath') ? $request->query->get('returnPath') :
|
||||||
$this->generateUrl('chill_main_homepage')
|
$this->generateUrl('chill_main_homepage')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,7 +249,7 @@ class UserController extends CRUDController
|
|||||||
|
|
||||||
return $this->redirect(
|
return $this->redirect(
|
||||||
$request->query->has('returnPath') ? $request->query->get('returnPath') :
|
$request->query->has('returnPath') ? $request->query->get('returnPath') :
|
||||||
$this->generateUrl('chill_crud_admin_user_edit', ['id' => $user->getId()])
|
$this->generateUrl('chill_crud_admin_user_edit', ['id' => $user->getId()])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,6 +264,7 @@ class UserController extends CRUDController
|
|||||||
return $this->getFilterOrderHelperFactory()
|
return $this->getFilterOrderHelperFactory()
|
||||||
->create(self::class)
|
->create(self::class)
|
||||||
->addSearchBox(['label'])
|
->addSearchBox(['label'])
|
||||||
|
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||||
->build();
|
->build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,11 +274,7 @@ class UserController extends CRUDController
|
|||||||
return parent::countEntities($action, $request, $filterOrder);
|
return parent::countEntities($action, $request, $filterOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $filterOrder->getQueryString()) {
|
return $this->userRepository->countFilteredUsers($filterOrder->getQueryString(), $filterOrder->getCheckboxData('activeFilter'));
|
||||||
return parent::countEntities($action, $request, $filterOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->userRepository->countByUsernameOrEmail($filterOrder->getQueryString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
|
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
|
||||||
@ -334,16 +331,13 @@ class UserController extends CRUDController
|
|||||||
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $filterOrder->getQueryString()) {
|
$queryString = $filterOrder->getQueryString();
|
||||||
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||||
}
|
$nb = $this->userRepository->countFilteredUsers($queryString, $activeFilter);
|
||||||
|
|
||||||
return $this->userRepository->findByUsernameOrEmail(
|
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||||
$filterOrder->getQueryString(),
|
|
||||||
['usernameCanonical' => 'ASC'],
|
return $this->userRepository->findFilteredUsers($queryString, $activeFilter, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage());
|
||||||
$paginator->getItemsPerPage(),
|
|
||||||
$paginator->getCurrentPageFirstItemNumber()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function onPrePersist(string $action, $entity, FormInterface $form, Request $request)
|
protected function onPrePersist(string $action, $entity, FormInterface $form, Request $request)
|
||||||
@ -374,10 +368,12 @@ class UserController extends CRUDController
|
|||||||
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
|
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
|
||||||
|
|
||||||
return $this->createFormBuilder()
|
return $this->createFormBuilder()
|
||||||
->setAction($this->generateUrl(
|
->setAction(
|
||||||
'admin_user_add_groupcenter',
|
$this->generateUrl(
|
||||||
array_merge($returnPathParams, ['uid' => $user->getId()])
|
'admin_user_add_groupcenter',
|
||||||
))
|
array_merge($returnPathParams, ['uid' => $user->getId()])
|
||||||
|
)
|
||||||
|
)
|
||||||
->setMethod('POST')
|
->setMethod('POST')
|
||||||
->add(self::FORM_GROUP_CENTER_COMPOSED, ComposedGroupCenterType::class)
|
->add(self::FORM_GROUP_CENTER_COMPOSED, ComposedGroupCenterType::class)
|
||||||
->add('submit', SubmitType::class, ['label' => 'Add a new groupCenter'])
|
->add('submit', SubmitType::class, ['label' => 'Add a new groupCenter'])
|
||||||
@ -392,10 +388,12 @@ class UserController extends CRUDController
|
|||||||
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
|
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
|
||||||
|
|
||||||
return $this->createFormBuilder()
|
return $this->createFormBuilder()
|
||||||
->setAction($this->generateUrl(
|
->setAction(
|
||||||
'admin_user_delete_groupcenter',
|
$this->generateUrl(
|
||||||
array_merge($returnPathParams, ['uid' => $user->getId(), 'gcid' => $groupCenter->getId()])
|
'admin_user_delete_groupcenter',
|
||||||
))
|
array_merge($returnPathParams, ['uid' => $user->getId(), 'gcid' => $groupCenter->getId()])
|
||||||
|
)
|
||||||
|
)
|
||||||
->setMethod('DELETE')
|
->setMethod('DELETE')
|
||||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||||
->getForm();
|
->getForm();
|
||||||
|
@ -24,9 +24,9 @@ interface CronJobInterface
|
|||||||
*
|
*
|
||||||
* If data is returned, this data is passed as argument on the next execution
|
* If data is returned, this data is passed as argument on the next execution
|
||||||
*
|
*
|
||||||
* @param array $lastExecutionData the data which was returned from the previous execution
|
* @param array<string|int, int|float|string|bool|array<int|string, int|float|string|bool>> $lastExecutionData the data which was returned from the previous execution
|
||||||
*
|
*
|
||||||
* @return array|null optionally return an array with the same data than the previous execution
|
* @return array<string|int, int|float|string|bool|array<int|string, int|float|string|bool>>|null optionally return an array with the same data than the previous execution
|
||||||
*/
|
*/
|
||||||
public function run(array $lastExecutionData): ?array;
|
public function run(array $lastExecutionData): ?array;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ namespace Chill\MainBundle\Repository;
|
|||||||
|
|
||||||
use Chill\MainBundle\Entity\Notification;
|
use Chill\MainBundle\Entity\Notification;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Doctrine\DBAL\Result;
|
||||||
use Doctrine\DBAL\Statement;
|
use Doctrine\DBAL\Statement;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@ -81,10 +83,7 @@ final class NotificationRepository implements ObjectRepository
|
|||||||
$results->free();
|
$results->free();
|
||||||
} else {
|
} else {
|
||||||
$wheres = [];
|
$wheres = [];
|
||||||
foreach ([
|
foreach ([['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId], ...$more] as $k => ['relatedEntityClass' => $relClass, 'relatedEntityId' => $relId]) {
|
||||||
['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId],
|
|
||||||
...$more,
|
|
||||||
] as $k => ['relatedEntityClass' => $relClass, 'relatedEntityId' => $relId]) {
|
|
||||||
$wheres[] = "(relatedEntityClass = :relatedEntityClass_{$k} AND relatedEntityId = :relatedEntityId_{$k})";
|
$wheres[] = "(relatedEntityClass = :relatedEntityClass_{$k} AND relatedEntityId = :relatedEntityId_{$k})";
|
||||||
$sqlParams["relatedEntityClass_{$k}"] = $relClass;
|
$sqlParams["relatedEntityClass_{$k}"] = $relClass;
|
||||||
$sqlParams["relatedEntityId_{$k}"] = $relId;
|
$sqlParams["relatedEntityId_{$k}"] = $relId;
|
||||||
@ -228,11 +227,11 @@ final class NotificationRepository implements ObjectRepository
|
|||||||
$rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
|
$rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
|
||||||
|
|
||||||
$sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '.
|
$sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '.
|
||||||
'FROM chill_main_notification cmn '.
|
'FROM chill_main_notification cmn '.
|
||||||
'WHERE '.
|
'WHERE '.
|
||||||
'EXISTS (select 1 FROM chill_main_notification_addresses_unread cmnau WHERE cmnau.user_id = :userId and cmnau.notification_id = cmn.id) '.
|
'EXISTS (select 1 FROM chill_main_notification_addresses_unread cmnau WHERE cmnau.user_id = :userId and cmnau.notification_id = cmn.id) '.
|
||||||
'ORDER BY cmn.date DESC '.
|
'ORDER BY cmn.date DESC '.
|
||||||
'LIMIT :limit OFFSET :offset';
|
'LIMIT :limit OFFSET :offset';
|
||||||
|
|
||||||
$nq = $this->em->createNativeQuery($sql, $rsm)
|
$nq = $this->em->createNativeQuery($sql, $rsm)
|
||||||
->setParameter('userId', $user->getId())
|
->setParameter('userId', $user->getId())
|
||||||
@ -255,10 +254,12 @@ final class NotificationRepository implements ObjectRepository
|
|||||||
$qb = $this->repository->createQueryBuilder('n');
|
$qb = $this->repository->createQueryBuilder('n');
|
||||||
|
|
||||||
// add condition for related entity (in main arguments, and in more)
|
// add condition for related entity (in main arguments, and in more)
|
||||||
$or = $qb->expr()->orX($qb->expr()->andX(
|
$or = $qb->expr()->orX(
|
||||||
$qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'),
|
$qb->expr()->andX(
|
||||||
$qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')
|
$qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'),
|
||||||
));
|
$qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')
|
||||||
|
)
|
||||||
|
);
|
||||||
$qb
|
$qb
|
||||||
->setParameter('relatedEntityClass', $relatedEntityClass)
|
->setParameter('relatedEntityClass', $relatedEntityClass)
|
||||||
->setParameter('relatedEntityId', $relatedEntityId);
|
->setParameter('relatedEntityId', $relatedEntityId);
|
||||||
@ -310,4 +311,86 @@ final class NotificationRepository implements ObjectRepository
|
|||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<int> the ids of the notifications marked as unread
|
||||||
|
*/
|
||||||
|
public function markAllNotificationAsReadForUser(User $user): array
|
||||||
|
{
|
||||||
|
// Get the database connection from the entity manager
|
||||||
|
$connection = $this->em->getConnection();
|
||||||
|
|
||||||
|
/** @var Result $results */
|
||||||
|
$results = $connection->transactional(function (Connection $connection) use ($user) {
|
||||||
|
// Define the SQL query
|
||||||
|
$sql = <<<'SQL'
|
||||||
|
DELETE FROM chill_main_notification_addresses_unread
|
||||||
|
WHERE user_id = :user_id
|
||||||
|
RETURNING notification_id
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
return $connection->executeQuery($sql, ['user_id' => $user->getId()]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$notificationIdsTouched = [];
|
||||||
|
foreach ($results->iterateAssociative() as $row) {
|
||||||
|
$notificationIdsTouched[] = $row['notification_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($notificationIdsTouched);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int> $notificationIds
|
||||||
|
*/
|
||||||
|
public function markAllNotificationAsUnreadForUser(User $user, array $notificationIds): array
|
||||||
|
{
|
||||||
|
// Get the database connection from the entity manager
|
||||||
|
$connection = $this->em->getConnection();
|
||||||
|
|
||||||
|
/** @var Result $results */
|
||||||
|
$results = $connection->transactional(function (Connection $connection) use ($user, $notificationIds) {
|
||||||
|
// This query double-check that the user is one of the addresses of the notification or the sender,
|
||||||
|
// if the notification is already marked as unread, this query does not fails.
|
||||||
|
// this query return the list of notification id which are affected
|
||||||
|
$sql = <<<'SQL'
|
||||||
|
INSERT INTO chill_main_notification_addresses_unread (user_id, notification_id)
|
||||||
|
SELECT ?, chill_main_notification_addresses_user.notification_id
|
||||||
|
FROM chill_main_notification_addresses_user JOIN chill_main_notification ON chill_main_notification_addresses_user.notification_id = chill_main_notification.id
|
||||||
|
WHERE (chill_main_notification_addresses_user.user_id = ? OR chill_main_notification.sender_id = ?)
|
||||||
|
AND chill_main_notification_addresses_user.notification_id IN ({ notification_ids })
|
||||||
|
ON CONFLICT (user_id, notification_id) DO NOTHING
|
||||||
|
RETURNING notification_id
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
$params = [$user->getId(), $user->getId(), $user->getId(), ...array_values($notificationIds)];
|
||||||
|
$sql = strtr($sql, ['{ notification_ids }' => implode(', ', array_fill(0, count($notificationIds), '?'))]);
|
||||||
|
|
||||||
|
return $connection->executeQuery($sql, $params);
|
||||||
|
});
|
||||||
|
|
||||||
|
$notificationIdsTouched = [];
|
||||||
|
foreach ($results->iterateAssociative() as $row) {
|
||||||
|
$notificationIdsTouched[] = $row['notification_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($notificationIdsTouched);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findAllUnreadByUser(User $user): array
|
||||||
|
{
|
||||||
|
$rsm = new Query\ResultSetMappingBuilder($this->em);
|
||||||
|
$rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
|
||||||
|
|
||||||
|
$sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '.
|
||||||
|
'FROM chill_main_notification cmn '.
|
||||||
|
'WHERE '.
|
||||||
|
'EXISTS (SELECT 1 FROM chill_main_notification_addresses_unread cmnau WHERE cmnau.user_id = :userId AND cmnau.notification_id = cmn.id) '.
|
||||||
|
'ORDER BY cmn.date DESC';
|
||||||
|
|
||||||
|
$nq = $this->em->createNativeQuery($sql, $rsm)
|
||||||
|
->setParameter('userId', $user->getId());
|
||||||
|
|
||||||
|
return $nq->getResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ use Doctrine\DBAL\Connection;
|
|||||||
use Doctrine\DBAL\Exception;
|
use Doctrine\DBAL\Exception;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
use Doctrine\ORM\NoResultException;
|
use Doctrine\ORM\NoResultException;
|
||||||
use Doctrine\ORM\Query\ResultSetMapping;
|
use Doctrine\ORM\Query\ResultSetMapping;
|
||||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||||
@ -26,9 +27,25 @@ final readonly class UserRepository implements UserRepositoryInterface
|
|||||||
{
|
{
|
||||||
private EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
private const FIELDS = ['id', 'email', 'enabled', 'civility_id', 'civility_abbreviation', 'civility_name', 'label', 'mainCenter_id',
|
private const FIELDS = [
|
||||||
'mainCenter_name', 'mainScope_id', 'mainScope_name', 'userJob_id', 'userJob_name', 'currentLocation_id', 'currentLocation_name',
|
'id',
|
||||||
'mainLocation_id', 'mainLocation_name'];
|
'email',
|
||||||
|
'enabled',
|
||||||
|
'civility_id',
|
||||||
|
'civility_abbreviation',
|
||||||
|
'civility_name',
|
||||||
|
'label',
|
||||||
|
'mainCenter_id',
|
||||||
|
'mainCenter_name',
|
||||||
|
'mainScope_id',
|
||||||
|
'mainScope_name',
|
||||||
|
'userJob_id',
|
||||||
|
'userJob_name',
|
||||||
|
'currentLocation_id',
|
||||||
|
'currentLocation_name',
|
||||||
|
'mainLocation_id',
|
||||||
|
'mainLocation_name',
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(private EntityManagerInterface $entityManager, private Connection $connection)
|
public function __construct(private EntityManagerInterface $entityManager, private Connection $connection)
|
||||||
{
|
{
|
||||||
@ -296,6 +313,25 @@ final readonly class UserRepository implements UserRepositoryInterface
|
|||||||
return User::class;
|
return User::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getResult(
|
||||||
|
QueryBuilder $qb,
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = [],
|
||||||
|
): array {
|
||||||
|
$qb->select('u');
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->setFirstResult($start)
|
||||||
|
->setMaxResults($limit);
|
||||||
|
|
||||||
|
foreach ($orderBy as $field => $direction) {
|
||||||
|
$qb->addOrderBy('u.'.$field, $direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
private function queryByUsernameOrEmail(string $pattern): QueryBuilder
|
private function queryByUsernameOrEmail(string $pattern): QueryBuilder
|
||||||
{
|
{
|
||||||
$qb = $this->entityManager->createQueryBuilder()->from(User::class, 'u');
|
$qb = $this->entityManager->createQueryBuilder()->from(User::class, 'u');
|
||||||
@ -312,4 +348,49 @@ final readonly class UserRepository implements UserRepositoryInterface
|
|||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function buildFilterBaseQuery(?string $queryString, array $isActive)
|
||||||
|
{
|
||||||
|
if (null !== $queryString) {
|
||||||
|
$qb = $this->queryByUsernameOrEmail($queryString);
|
||||||
|
} else {
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()->from(User::class, 'u');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add condition based on active/inactive status
|
||||||
|
if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
|
||||||
|
$qb->andWhere('u.enabled = true');
|
||||||
|
} elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
|
||||||
|
$qb->andWhere('u.enabled = false');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findFilteredUsers(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = ['username' => 'ASC'],
|
||||||
|
): array {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countFilteredUsers(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
): int {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $qb
|
||||||
|
->select('COUNT(u)')
|
||||||
|
->getQuery()->getSingleScalarResult();
|
||||||
|
} catch (NoResultException|NonUniqueResultException $e) {
|
||||||
|
throw new \LogicException('a count query should return one result', previous: $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import {createApp} from "vue";
|
import { createApp } from "vue";
|
||||||
import NotificationReadToggle from "ChillMainAssets/vuejs/_components/Notification/NotificationReadToggle.vue";
|
import NotificationReadToggle from "ChillMainAssets/vuejs/_components/Notification/NotificationReadToggle.vue";
|
||||||
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
|
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
|
||||||
|
import NotificationReadAllToggle from "ChillMainAssets/vuejs/_components/Notification/NotificationReadAllToggle.vue";
|
||||||
|
|
||||||
const i18n = _createI18n({});
|
const i18n = _createI18n({});
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', function (e) {
|
window.addEventListener("DOMContentLoaded", function (e) {
|
||||||
document.querySelectorAll('.notification_toggle_read_status')
|
document
|
||||||
.forEach(function (el, i) {
|
.querySelectorAll(".notification_toggle_read_status")
|
||||||
createApp({
|
.forEach(function (el, i) {
|
||||||
template: `<notification-read-toggle
|
createApp({
|
||||||
|
template: `<notification-read-toggle
|
||||||
:notificationId="notificationId"
|
:notificationId="notificationId"
|
||||||
:buttonClass="buttonClass"
|
:buttonClass="buttonClass"
|
||||||
:buttonNoText="buttonNoText"
|
:buttonNoText="buttonNoText"
|
||||||
@ -17,40 +19,45 @@ window.addEventListener('DOMContentLoaded', function (e) {
|
|||||||
@markRead="onMarkRead"
|
@markRead="onMarkRead"
|
||||||
@markUnread="onMarkUnread">
|
@markUnread="onMarkUnread">
|
||||||
</notification-read-toggle>`,
|
</notification-read-toggle>`,
|
||||||
components: {
|
components: {
|
||||||
NotificationReadToggle,
|
NotificationReadToggle,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
notificationId: el.dataset.notificationId,
|
notificationId: parseInt(el.dataset.notificationId),
|
||||||
buttonClass: el.dataset.buttonClass,
|
buttonClass: el.dataset.buttonClass,
|
||||||
buttonNoText: 'false' === el.dataset.buttonText,
|
buttonNoText: "false" === el.dataset.buttonText,
|
||||||
showUrl: el.dataset.showButtonUrl,
|
showUrl: el.dataset.showButtonUrl,
|
||||||
isRead: 1 === Number.parseInt(el.dataset.notificationCurrentIsRead),
|
isRead: 1 === Number.parseInt(el.dataset.notificationCurrentIsRead),
|
||||||
container: el.dataset.container
|
container: el.dataset.container,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
getContainer() {
|
getContainer() {
|
||||||
return document.querySelectorAll(`div.${this.container}`);
|
return document.querySelectorAll(`div.${this.container}`);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onMarkRead() {
|
onMarkRead() {
|
||||||
if (typeof this.getContainer[i] !== 'undefined') {
|
if (typeof this.getContainer[i] !== "undefined") {
|
||||||
this.getContainer[i].classList.replace('read', 'unread');
|
this.getContainer[i].classList.replace("read", "unread");
|
||||||
} else { throw 'data-container attribute is missing' }
|
} else {
|
||||||
this.isRead = false;
|
throw "data-container attribute is missing";
|
||||||
},
|
}
|
||||||
onMarkUnread() {
|
this.isRead = false;
|
||||||
if (typeof this.getContainer[i] !== 'undefined') {
|
},
|
||||||
this.getContainer[i].classList.replace('unread', 'read');
|
onMarkUnread() {
|
||||||
} else { throw 'data-container attribute is missing' }
|
if (typeof this.getContainer[i] !== "undefined") {
|
||||||
this.isRead = true;
|
this.getContainer[i].classList.replace("unread", "read");
|
||||||
},
|
} else {
|
||||||
}
|
throw "data-container attribute is missing";
|
||||||
})
|
}
|
||||||
.use(i18n)
|
this.isRead = true;
|
||||||
.mount(el);
|
},
|
||||||
});
|
},
|
||||||
|
})
|
||||||
|
.use(i18n)
|
||||||
|
.mount(el);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import { _createI18n } from "../../vuejs/_js/i18n";
|
||||||
|
import NotificationReadAllToggle from "../../vuejs/_components/Notification/NotificationReadAllToggle.vue";
|
||||||
|
|
||||||
|
const i18n = _createI18n({});
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const elements = document.querySelectorAll(".notification_all_read");
|
||||||
|
|
||||||
|
elements.forEach((element) => {
|
||||||
|
console.log('launch');
|
||||||
|
createApp({
|
||||||
|
template: `<notification-read-all-toggle @markAsRead="markAsRead" @markAsUnRead="markAsUnread"></notification-read-all-toggle>`,
|
||||||
|
components: {
|
||||||
|
NotificationReadAllToggle,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
markAsRead(id: number) {
|
||||||
|
const el = document.querySelector<HTMLDivElement>(`div.notification-status[data-notification-id="${id}"]`);
|
||||||
|
if (el === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.classList.add('read');
|
||||||
|
el.classList.remove('unread');
|
||||||
|
},
|
||||||
|
markAsUnread(id: number) {
|
||||||
|
const el = document.querySelector<HTMLDivElement>(`div.notification-status[data-notification-id="${id}"]`);
|
||||||
|
if (el === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.classList.remove('read');
|
||||||
|
el.classList.add('unread');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.use(i18n)
|
||||||
|
.mount(element);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button v-if="idsMarkedAsRead.length === 0"
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
@click="markAllRead"
|
||||||
|
>
|
||||||
|
<i class="fa fa-sm fa-envelope-open-o"></i> Marquer tout comme lu
|
||||||
|
</button>
|
||||||
|
<button v-else
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
@click="undo"
|
||||||
|
>
|
||||||
|
<i class="fa fa-sm fa-envelope-open-o"></i> Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { makeFetch } from "../../../lib/api/apiMethods";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'markAsRead', id: number): void,
|
||||||
|
(e: 'markAsUnRead', id: number): void,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const idsMarkedAsRead = ref([] as number[]);
|
||||||
|
|
||||||
|
async function markAllRead() {
|
||||||
|
const ids: number[] = await makeFetch("POST", `/api/1.0/main/notification/mark/allread`, null);
|
||||||
|
for (let i of ids) {
|
||||||
|
idsMarkedAsRead.value.push(i);
|
||||||
|
emit('markAsRead', i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function undo() {
|
||||||
|
const touched: number[] = await makeFetch("POST", `/api/1.0/main/notification/mark/undoallread`, idsMarkedAsRead.value);
|
||||||
|
while (idsMarkedAsRead.value.length > 0) {
|
||||||
|
idsMarkedAsRead.value.pop();
|
||||||
|
}
|
||||||
|
for (let t of touched) {
|
||||||
|
emit('markAsUnRead', t);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
@ -1,47 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="{'btn-group btn-group-sm float-end': isButtonGroup }"
|
<div
|
||||||
role="group" aria-label="Notification actions">
|
:class="{ 'btn-group btn-group-sm float-end': isButtonGroup }"
|
||||||
|
role="group"
|
||||||
<button v-if="isRead"
|
aria-label="Notification actions"
|
||||||
class="btn"
|
>
|
||||||
:class="overrideClass"
|
<button
|
||||||
type="button"
|
v-if="isRead"
|
||||||
:title="$t('markAsUnread')"
|
class="btn"
|
||||||
@click="markAsUnread"
|
:class="overrideClass"
|
||||||
>
|
type="button"
|
||||||
|
:title="$t('markAsUnread')"
|
||||||
|
@click="markAsUnread"
|
||||||
|
>
|
||||||
<i class="fa fa-sm fa-envelope-o"></i>
|
<i class="fa fa-sm fa-envelope-o"></i>
|
||||||
<span v-if="!buttonNoText" class="ps-2">
|
<span v-if="!buttonNoText" class="ps-2">
|
||||||
{{ $t('markAsUnread') }}
|
{{ $t("markAsUnread") }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="!isRead"
|
<button
|
||||||
class="btn"
|
v-if="!isRead"
|
||||||
:class="overrideClass"
|
class="btn"
|
||||||
type="button"
|
:class="overrideClass"
|
||||||
:title="$t('markAsRead')"
|
type="button"
|
||||||
@click="markAsRead"
|
:title="$t('markAsRead')"
|
||||||
>
|
@click="markAsRead"
|
||||||
|
>
|
||||||
<i class="fa fa-sm fa-envelope-open-o"></i>
|
<i class="fa fa-sm fa-envelope-open-o"></i>
|
||||||
<span v-if="!buttonNoText" class="ps-2">
|
<span v-if="!buttonNoText" class="ps-2">
|
||||||
{{ $t('markAsRead') }}
|
{{ $t("markAsRead") }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a v-if="isButtonGroup"
|
<a
|
||||||
|
v-if="isButtonGroup"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-primary"
|
class="btn btn-outline-primary"
|
||||||
:href="showUrl"
|
:href="showUrl"
|
||||||
:title="$t('action.show')"
|
:title="$t('action.show')"
|
||||||
>
|
>
|
||||||
<i class="fa fa-sm fa-comment-o"></i>
|
<i class="fa fa-sm fa-comment-o"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<!-- "Mark All Read" button -->
|
||||||
|
<button
|
||||||
|
v-if="showMarkAllButton"
|
||||||
|
class="btn"
|
||||||
|
:class="overrideClass"
|
||||||
|
type="button"
|
||||||
|
:title="$t('markAllRead')"
|
||||||
|
@click="markAllRead"
|
||||||
|
>
|
||||||
|
<i class="fa fa-sm fa-envelope-o"></i>
|
||||||
|
<span v-if="!buttonNoText" class="ps-2">
|
||||||
|
{{ $t("markAllRead") }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods.ts';
|
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods.ts";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "NotificationReadToggle",
|
name: "NotificationReadToggle",
|
||||||
@ -57,7 +76,7 @@ export default {
|
|||||||
// Optional
|
// Optional
|
||||||
buttonClass: {
|
buttonClass: {
|
||||||
required: false,
|
required: false,
|
||||||
type: String
|
type: String,
|
||||||
},
|
},
|
||||||
buttonNoText: {
|
buttonNoText: {
|
||||||
required: false,
|
required: false,
|
||||||
@ -65,14 +84,14 @@ export default {
|
|||||||
},
|
},
|
||||||
showUrl: {
|
showUrl: {
|
||||||
required: false,
|
required: false,
|
||||||
type: String
|
type: String,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
emits: ['markRead', 'markUnread'],
|
emits: ["markRead", "markUnread"],
|
||||||
computed: {
|
computed: {
|
||||||
/// [Option] override default button appearance (btn-misc)
|
/// [Option] override default button appearance (btn-misc)
|
||||||
overrideClass() {
|
overrideClass() {
|
||||||
return this.buttonClass ? this.buttonClass : 'btn-misc'
|
return this.buttonClass ? this.buttonClass : "btn-misc";
|
||||||
},
|
},
|
||||||
/// [Option] don't display text on button
|
/// [Option] don't display text on button
|
||||||
buttonHideText() {
|
buttonHideText() {
|
||||||
@ -82,31 +101,48 @@ export default {
|
|||||||
// When passed, the component return a button-group with 2 buttons.
|
// When passed, the component return a button-group with 2 buttons.
|
||||||
isButtonGroup() {
|
isButtonGroup() {
|
||||||
return this.showUrl;
|
return this.showUrl;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
markAsUnread() {
|
markAsUnread() {
|
||||||
makeFetch('POST', `/api/1.0/main/notification/${this.notificationId}/mark/unread`, []).then(response => {
|
makeFetch(
|
||||||
this.$emit('markRead', { notificationId: this.notificationId });
|
"POST",
|
||||||
})
|
`/api/1.0/main/notification/${this.notificationId}/mark/unread`,
|
||||||
|
[]
|
||||||
|
).then((response) => {
|
||||||
|
this.$emit("markRead", {notificationId: this.notificationId});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
markAsRead() {
|
markAsRead() {
|
||||||
makeFetch('POST', `/api/1.0/main/notification/${this.notificationId}/mark/read`, []).then(response => {
|
makeFetch(
|
||||||
this.$emit('markUnread', { notificationId: this.notificationId });
|
"POST",
|
||||||
})
|
`/api/1.0/main/notification/${this.notificationId}/mark/read`,
|
||||||
|
[]
|
||||||
|
).then((response) => {
|
||||||
|
this.$emit("markUnread", {
|
||||||
|
notificationId: this.notificationId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
markAllRead() {
|
||||||
|
makeFetch(
|
||||||
|
"POST",
|
||||||
|
`/api/1.0/main/notification/markallread`,
|
||||||
|
[]
|
||||||
|
).then((response) => {
|
||||||
|
this.$emit("markAllRead");
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
messages: {
|
messages: {
|
||||||
fr: {
|
fr: {
|
||||||
markAsUnread: 'Marquer comme non-lu',
|
markAsUnread: "Marquer comme non-lu",
|
||||||
markAsRead: 'Marquer comme lu'
|
markAsRead: "Marquer comme lu",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss"></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
@ -1,30 +1,33 @@
|
|||||||
{% macro title(c) %}
|
{% macro title(c) %}
|
||||||
<div class="item-row title">
|
<div class="item-row title">
|
||||||
<h2 class="notification-title">
|
<h2 class="notification-title">
|
||||||
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">
|
<a
|
||||||
|
href="{{ chill_path_add_return_path('chill_main_notification_show', {
|
||||||
|
id: c.notification.id
|
||||||
|
}) }}"
|
||||||
|
>
|
||||||
{{ c.notification.title }}
|
{{ c.notification.title }}
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro header(c) %}
|
{% macro header(c) %}
|
||||||
<div class="item-row notification-header mt-2">
|
<div class="item-row notification-header mt-2">
|
||||||
<div class="item-col">
|
<div class="item-col">
|
||||||
<ul class="small_in_title">
|
<ul class="small_in_title">
|
||||||
{% if c.step is not defined or c.step == 'inbox' %}
|
{% if c.step is not defined or c.step == 'inbox' %}
|
||||||
<li class="notification-from">
|
<li class="notification-from">
|
||||||
<span class="item-key">
|
<span class="item-key">
|
||||||
<abbr title="{{ 'notification.received_from'|trans }}">
|
<abbr title="{{ 'notification.received_from' | trans }}">
|
||||||
{{ 'notification.from'|trans }} :
|
{{ "notification.from" | trans }} :
|
||||||
</abbr>
|
</abbr>
|
||||||
</span>
|
</span>
|
||||||
{% if not c.notification.isSystem %}
|
{% if not c.notification.isSystem %}
|
||||||
<span class="badge-user">
|
<span class="badge-user">
|
||||||
{{ c.notification.sender|chill_entity_render_string({'at_date': c.notification.date}) }}
|
{{ c.notification.sender | chill_entity_render_string({'at_date': c.notification.date}) }}
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge-user system">{{ 'notification.is_system'|trans }}</span>
|
<span class="badge-user system">{{ "notification.is_system" | trans }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -32,34 +35,37 @@
|
|||||||
<li class="notification-to">
|
<li class="notification-to">
|
||||||
{% if c.notification_cc is defined %}
|
{% if c.notification_cc is defined %}
|
||||||
{% if c.notification_cc %}
|
{% if c.notification_cc %}
|
||||||
<span class="item-key">
|
<span class="item-key">
|
||||||
<abbr title="{{ 'notification.sent_cc'|trans }}">
|
<abbr title="{{ 'notification.sent_cc' | trans }}">
|
||||||
{{ 'notification.cc'|trans }} :
|
{{ "notification.cc" | trans }} :
|
||||||
</abbr>
|
</abbr>
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="item-key">
|
<span class="item-key">
|
||||||
<abbr title="{{ 'notification.sent_to'|trans }}">
|
<abbr title="{{ 'notification.sent_to' | trans }}">
|
||||||
{{ 'notification.to'|trans }} :
|
{{ "notification.to" | trans }} :
|
||||||
</abbr>
|
</abbr>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="item-key">
|
<span class="item-key">
|
||||||
<abbr title="{{ 'notification.sent_to'|trans }}">
|
<abbr title="{{ 'notification.sent_to' | trans }}">
|
||||||
{{ 'notification.to'|trans }} :
|
{{ "notification.to" | trans }} :
|
||||||
</abbr>
|
</abbr>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for a in c.notification.addressees %}
|
{% for a in c.notification.addressees %}
|
||||||
<span class="badge-user">
|
<span class="badge-user">
|
||||||
{{ a|chill_entity_render_string({'at_date': c.notification.date}) }}
|
{{ a | chill_entity_render_string({'at_date': c.notification.date}) }}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for a in c.notification.addressesEmails %}
|
{% for a in c.notification.addressesEmails %}
|
||||||
<span class="badge-user" title="{{ 'notification.Email with access link'|trans|e('html_attr') }}">
|
<span
|
||||||
{{ a }}
|
class="badge-user"
|
||||||
</span>
|
title="{{ 'notification.Email with access link'|trans|e('html_attr') }}"
|
||||||
|
>
|
||||||
|
{{ a }}
|
||||||
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -70,7 +76,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro content(c) %}
|
{% macro content(c) %}
|
||||||
<div class="item-row separator">
|
<div class="item-row separator">
|
||||||
{% if c.data is defined %}
|
{% if c.data is defined %}
|
||||||
@ -83,60 +88,77 @@
|
|||||||
<div class="notification-content">
|
<div class="notification-content">
|
||||||
{% if c.full_content is defined and c.full_content == true %}
|
{% if c.full_content is defined and c.full_content == true %}
|
||||||
{% if c.notification.message is not empty %}
|
{% if c.notification.message is not empty %}
|
||||||
{{ c.notification.message|chill_markdown_to_html }}
|
{{ c.notification.message | chill_markdown_to_html }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="chill-no-data-statement">{{ 'Any comment'|trans }}</p>
|
<p class="chill-no-data-statement">{{ "Any comment" | trans }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if c.notification.message is not empty %}
|
{% if c.notification.message is not empty %}
|
||||||
{{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
|
{{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
|
||||||
<p class="read-more"><a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">{{ 'Read more'|trans }}</a></p>
|
<p class="read-more">
|
||||||
|
<a
|
||||||
|
href="{{ chill_path_add_return_path('chill_main_notification_show', {
|
||||||
|
id: c.notification.id
|
||||||
|
}) }}"
|
||||||
|
>{{ "Read more" | trans }}</a>
|
||||||
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="chill-no-data-statement">{{ 'Any comment'|trans }}</p>
|
<p class="chill-no-data-statement">{{ "Any comment" | trans }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro actions(c) %}
|
{% macro actions(c) %}
|
||||||
{% if c.action_button is not defined or c.action_button != false %}
|
{% if c.action_button is not defined or c.action_button != false %}
|
||||||
<div class="item-row separator">
|
<div class="item-row separator">
|
||||||
<div class="item-col item-meta">
|
<div class="item-col item-meta">
|
||||||
|
|
||||||
{% if c.notification.comments|length > 0 %}
|
{% if c.notification.comments|length > 0 %}
|
||||||
<div class="comment-counter">
|
<div class="comment-counter">
|
||||||
<span class="counter">
|
<span class="counter">
|
||||||
{{ 'notification.counter comments'|trans({'nb': c.notification.comments|length }) }}
|
{{ 'notification.counter comments'|trans({'nb': c.notification.comments|length }) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="item-col">
|
<div class="item-col">
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
<li>
|
<li>
|
||||||
{# Vue component #}
|
{# Vue component #}
|
||||||
<span class="notification_toggle_read_status"
|
<span
|
||||||
data-notification-id="{{ c.notification.id }}"
|
class="notification_toggle_read_status"
|
||||||
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
|
data-notification-id="{{ c.notification.id }}"
|
||||||
data-container="notification-status"
|
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
|
||||||
|
data-container="notification-status"
|
||||||
></span>
|
></span>
|
||||||
</li>
|
</li>
|
||||||
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %}
|
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ chill_path_add_return_path('chill_main_notification_edit', {'id': c.notification.id}) }}"
|
<a
|
||||||
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
href="{{ chill_path_add_return_path(
|
||||||
|
'chill_main_notification_edit',
|
||||||
|
{ id: c.notification.id }
|
||||||
|
) }}"
|
||||||
|
class="btn btn-edit"
|
||||||
|
title="{{ 'Edit' | trans }}"
|
||||||
|
></a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', c.notification) %}
|
{% if is_granted('CHILL_MAIN_NOTIFICATION_SEE',
|
||||||
|
c.notification) %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}"
|
<a
|
||||||
class="btn {% if not c.notification.isSystem %}btn-show change-icon{% else %}btn-misc{% endif %}" title="{{ 'notification.see_comments_thread'|trans }}">
|
href="{{ chill_path_add_return_path(
|
||||||
|
'chill_main_notification_show',
|
||||||
|
{ id: c.notification.id }
|
||||||
|
) }}"
|
||||||
|
class="btn {% if not c.notification.isSystem %}btn-show change-icon{% else %}btn-misc{% endif %}"
|
||||||
|
title="{{ 'notification.see_comments_thread' | trans }}"
|
||||||
|
>
|
||||||
{% if not c.notification.isSystem() %}
|
{% if not c.notification.isSystem() %}
|
||||||
<i class="fa fa-comment"></i>
|
<i class="fa fa-comment"></i>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ 'Read more'|trans }}
|
{{ "Read more" | trans }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -147,24 +169,30 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
<div class="item-bloc notification-status {% if notification.isReadBy(app.user) %}read{% else %}unread{% endif %}">
|
<div
|
||||||
|
class="item-bloc notification-status {% if notification.isReadBy(app.user) %}read{% else %}unread{% endif %}"
|
||||||
|
data-notification-id="{{ notification.id|escape('html_attr') }}"
|
||||||
|
>
|
||||||
{% if fold_item is defined and fold_item != false %}
|
{% if fold_item is defined and fold_item != false %}
|
||||||
<div class="accordion-header" id="flush-heading-{{ notification.id }}">
|
<div class="accordion-header" id="flush-heading-{{ notification.id }}">
|
||||||
<button type="button" class="accordion-button collapsed"
|
<button
|
||||||
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ notification.id }}"
|
type="button"
|
||||||
aria-expanded="false" aria-controls="flush-collapse-{{ notification.id }}">
|
class="accordion-button collapsed"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#flush-collapse-{{ notification.id }}"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="flush-collapse-{{ notification.id }}"
|
||||||
|
>
|
||||||
{{ _self.title(_context) }}
|
{{ _self.title(_context) }}
|
||||||
</button>
|
</button>
|
||||||
{{ _self.header(_context) }}
|
{{ _self.header(_context) }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div id="flush-collapse-{{ notification.id }}"
|
<div
|
||||||
|
id="flush-collapse-{{ notification.id }}"
|
||||||
class="accordion-collapse collapse"
|
class="accordion-collapse collapse"
|
||||||
aria-labelledby="flush-heading-{{ notification.id }}"
|
aria-labelledby="flush-heading-{{ notification.id }}"
|
||||||
data-bs-parent="#notification-fold">
|
data-bs-parent="#notification-fold"
|
||||||
|
>
|
||||||
{{ _self.content(_context) }}
|
{{ _self.content(_context) }}
|
||||||
</div>
|
</div>
|
||||||
{{ _self.actions(_context) }}
|
{{ _self.actions(_context) }}
|
||||||
@ -174,5 +202,4 @@
|
|||||||
{{ _self.content(_context) }}
|
{{ _self.content(_context) }}
|
||||||
{{ _self.actions(_context) }}
|
{{ _self.actions(_context) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,62 +1,78 @@
|
|||||||
{% extends "@ChillMain/layout.html.twig" %}
|
{% extends "@ChillMain/layout.html.twig" %}
|
||||||
|
|
||||||
{% block title 'notification.My own notifications'|trans %}
|
{% block title 'notification.My own notifications'|trans %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
|
{{ encore_entry_script_tags("mod_notification_toggle_read_status") }}
|
||||||
|
{{ encore_entry_script_tags("mod_notification_toggle_read_all_status") }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
|
{{ encore_entry_link_tags("mod_notification_toggle_read_status") }}
|
||||||
|
{{ encore_entry_link_tags("mod_notification_toggle_read_all_status") }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-10 notification notification-list">
|
<div class="col-10 notification notification-list">
|
||||||
<h1>{{ block('title') }}</h1>
|
<h1>{{ block("title") }}</h1>
|
||||||
|
<ul class="nav nav-pills justify-content-center">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {% if step == 'inbox' %}active{% endif %}"
|
||||||
|
href="{{ path('chill_main_notification_my') }}"
|
||||||
|
>
|
||||||
|
{{ "notification.Notifications received" | trans }}
|
||||||
|
{% if unreads['inbox'] > 0 %}
|
||||||
|
<span class="badge rounded-pill bg-danger">
|
||||||
|
{{ unreads["inbox"] }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {% if step == 'sent' %}active{% endif %}"
|
||||||
|
href="{{ path('chill_main_notification_sent') }}"
|
||||||
|
>
|
||||||
|
{{ "notification.Notifications sent" | trans }}
|
||||||
|
{% if unreads['sent'] > 0 %}
|
||||||
|
<span class="badge rounded-pill bg-danger">
|
||||||
|
{{ unreads["sent"] }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<ul class="nav nav-pills justify-content-center">
|
{% if datas|length == 0 %} {% if step == 'inbox' %}
|
||||||
<li class="nav-item">
|
<p class="chill-no-data-statement">
|
||||||
<a class="nav-link {% if step == 'inbox' %}active{% endif %}" href="{{ path('chill_main_notification_my') }}">
|
{{ "notification.Any notification received" | trans }}
|
||||||
{{ 'notification.Notifications received'|trans }}
|
</p>
|
||||||
{% if unreads['inbox'] > 0 %}
|
|
||||||
<span class="badge rounded-pill bg-danger">
|
|
||||||
{{ unreads['inbox'] }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if step == 'sent' %}active{% endif %}" href="{{ path('chill_main_notification_sent') }}">
|
|
||||||
{{ 'notification.Notifications sent'|trans }}
|
|
||||||
{% if unreads['sent'] > 0 %}
|
|
||||||
<span class="badge rounded-pill bg-danger">
|
|
||||||
{{ unreads['sent'] }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{% if datas|length == 0 %}
|
|
||||||
{% if step == 'inbox' %}
|
|
||||||
<p class="chill-no-data-statement">{{ 'notification.Any notification received'|trans }}</p>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="chill-no-data-statement">{{ 'notification.Any notification sent'|trans }}</p>
|
<p class="chill-no-data-statement">
|
||||||
|
{{ "notification.Any notification sent" | trans }}
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="flex-table accordion accordion-flush" id="notification-fold">
|
<div class="flex-table accordion accordion-flush" id="notification-fold">
|
||||||
{% for data in datas %}
|
{% for data in datas %}
|
||||||
{% set notification = data.notification %}
|
{% set notification = data.notification %}
|
||||||
{% include '@ChillMain/Notification/_list_item.html.twig' with {
|
{% include '@ChillMain/Notification/_list_item.html.twig' with {
|
||||||
'fold_item': true,
|
'fold_item': true, 'notification_cc': data.template_data.notificationCc
|
||||||
'notification_cc': data.template_data.notificationCc is defined ? data.template_data.notificationCc : false
|
is defined ? data.template_data.notificationCc : false } %}
|
||||||
} %}
|
{% endfor %}
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
|
||||||
|
{{ chill_pagination(paginator) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<ul class="record_actions sticky-form-buttons justify-content-end">
|
||||||
|
<li class="ml-auto d-flex align-items-center gap-2">
|
||||||
|
<span class="notification_all_read"></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ chill_pagination(paginator) }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -65,7 +65,7 @@ class PasswordRecoverLocker
|
|||||||
|
|
||||||
if (0 === $this->chillRedis->exists($key)) {
|
if (0 === $this->chillRedis->exists($key)) {
|
||||||
$this->chillRedis->set($key, 1);
|
$this->chillRedis->set($key, 1);
|
||||||
$this->chillRedis->setTimeout($key, $ttl);
|
$this->chillRedis->expire($key, $ttl);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,12 @@ use Doctrine\DBAL\Connection;
|
|||||||
use Doctrine\DBAL\Statement;
|
use Doctrine\DBAL\Statement;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import addresses into the database.
|
||||||
|
*
|
||||||
|
* This importer do some optimization about the import, ensuring that adresses are inserted and reconciled with
|
||||||
|
* the existing one on a optimized way.
|
||||||
|
*/
|
||||||
final class AddressReferenceBaseImporter
|
final class AddressReferenceBaseImporter
|
||||||
{
|
{
|
||||||
private const INSERT = <<<'SQL'
|
private const INSERT = <<<'SQL'
|
||||||
@ -47,11 +53,18 @@ final class AddressReferenceBaseImporter
|
|||||||
|
|
||||||
public function __construct(private readonly Connection $defaultConnection, private readonly LoggerInterface $logger) {}
|
public function __construct(private readonly Connection $defaultConnection, private readonly LoggerInterface $logger) {}
|
||||||
|
|
||||||
public function finalize(): void
|
/**
|
||||||
|
* Finalize the import process and make reconciliation with addresses.
|
||||||
|
*
|
||||||
|
* @param bool $allowRemoveDoubleRefId if true, allow the importer to remove automatically addresses with same refid
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function finalize(bool $allowRemoveDoubleRefId = false): void
|
||||||
{
|
{
|
||||||
$this->doInsertPending();
|
$this->doInsertPending();
|
||||||
|
|
||||||
$this->updateAddressReferenceTable();
|
$this->updateAddressReferenceTable($allowRemoveDoubleRefId);
|
||||||
|
|
||||||
$this->deleteTemporaryTable();
|
$this->deleteTemporaryTable();
|
||||||
|
|
||||||
@ -59,6 +72,11 @@ final class AddressReferenceBaseImporter
|
|||||||
$this->isInitialized = false;
|
$this->isInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do import a single address.
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
public function importAddress(
|
public function importAddress(
|
||||||
string $refAddress,
|
string $refAddress,
|
||||||
?string $refPostalCode,
|
?string $refPostalCode,
|
||||||
@ -167,15 +185,48 @@ final class AddressReferenceBaseImporter
|
|||||||
$this->isInitialized = true;
|
$this->isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateAddressReferenceTable(): void
|
private function updateAddressReferenceTable(bool $allowRemoveDoubleRefId): void
|
||||||
{
|
{
|
||||||
$this->defaultConnection->executeStatement(
|
$this->defaultConnection->executeStatement(
|
||||||
'CREATE INDEX idx_ref_add_temp ON reference_address_temp (refid)'
|
'CREATE INDEX idx_ref_add_temp ON reference_address_temp (refid)'
|
||||||
);
|
);
|
||||||
|
|
||||||
// 1) Add new addresses
|
// 0) detect for doublon in current temporary table
|
||||||
$this->logger->info(self::LOG_PREFIX.'upsert new addresses');
|
$results = $this->defaultConnection->executeQuery(
|
||||||
$affected = $this->defaultConnection->executeStatement("INSERT INTO chill_main_address_reference
|
'SELECT COUNT(*) AS nb_appearance, refid FROM reference_address_temp GROUP BY refid HAVING count(*) > 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
$hasDouble = false;
|
||||||
|
foreach ($results->iterateAssociative() as $result) {
|
||||||
|
$this->logger->error(self::LOG_PREFIX.'Some reference id are present more than one time', ['nb_apparearance' => $result['nb_appearance'], 'refid' => $result['refid']]);
|
||||||
|
$hasDouble = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasDouble) {
|
||||||
|
if ($allowRemoveDoubleRefId) {
|
||||||
|
$this->logger->alert(self::LOG_PREFIX.'We are going to remove the addresses which are present more than once in the table');
|
||||||
|
$this->defaultConnection->executeStatement('ALTER TABLE reference_address_temp ADD COLUMN gid SERIAL');
|
||||||
|
$removed = $this->defaultConnection->executeStatement(<<<'SQL'
|
||||||
|
WITH ordering AS (
|
||||||
|
SELECT gid, rank() over (PARTITION BY refid ORDER BY gid DESC) AS ranking
|
||||||
|
FROM reference_address_temp
|
||||||
|
WHERE refid IN (SELECT refid FROM reference_address_temp group by refid having count(*) > 1)
|
||||||
|
),
|
||||||
|
keep_last AS (
|
||||||
|
SELECT gid, ranking FROM ordering where ranking > 1
|
||||||
|
)
|
||||||
|
DELETE FROM reference_address_temp WHERE gid IN (SELECT gid FROM keep_last);
|
||||||
|
SQL);
|
||||||
|
$this->logger->alert(self::LOG_PREFIX.'addresses with same refid present twice, we removed some double', ['nb_removed', $removed]);
|
||||||
|
} else {
|
||||||
|
throw new \RuntimeException('Some addresses are present twice in the database, we cannot process them');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->defaultConnection->transactional(function ($connection): void {
|
||||||
|
// 1) Add new addresses
|
||||||
|
$this->logger->info(self::LOG_PREFIX.'upsert new addresses');
|
||||||
|
$affected = $connection->executeStatement("INSERT INTO chill_main_address_reference
|
||||||
(id, postcode_id, refid, street, streetnumber, municipalitycode, source, point, createdat, deletedat, updatedat)
|
(id, postcode_id, refid, street, streetnumber, municipalitycode, source, point, createdat, deletedat, updatedat)
|
||||||
SELECT
|
SELECT
|
||||||
nextval('chill_main_address_reference_id_seq'),
|
nextval('chill_main_address_reference_id_seq'),
|
||||||
@ -193,16 +244,17 @@ final class AddressReferenceBaseImporter
|
|||||||
ON CONFLICT (refid, source) DO UPDATE
|
ON CONFLICT (refid, source) DO UPDATE
|
||||||
SET postcode_id = excluded.postcode_id, refid = excluded.refid, street = excluded.street, streetnumber = excluded.streetnumber, municipalitycode = excluded.municipalitycode, source = excluded.source, point = excluded.point, updatedat = NOW(), deletedAt = NULL
|
SET postcode_id = excluded.postcode_id, refid = excluded.refid, street = excluded.street, streetnumber = excluded.streetnumber, municipalitycode = excluded.municipalitycode, source = excluded.source, point = excluded.point, updatedat = NOW(), deletedAt = NULL
|
||||||
");
|
");
|
||||||
$this->logger->info(self::LOG_PREFIX.'addresses upserted', ['upserted' => $affected]);
|
$this->logger->info(self::LOG_PREFIX.'addresses upserted', ['upserted' => $affected]);
|
||||||
|
|
||||||
// 3) Delete addresses
|
// 3) Delete addresses
|
||||||
$this->logger->info(self::LOG_PREFIX.'soft delete adresses');
|
$this->logger->info(self::LOG_PREFIX.'soft delete adresses');
|
||||||
$affected = $this->defaultConnection->executeStatement('UPDATE chill_main_address_reference
|
$affected = $connection->executeStatement('UPDATE chill_main_address_reference
|
||||||
SET deletedat = NOW()
|
SET deletedat = NOW()
|
||||||
WHERE
|
WHERE
|
||||||
chill_main_address_reference.refid NOT IN (SELECT refid FROM reference_address_temp WHERE source LIKE ?)
|
chill_main_address_reference.refid NOT IN (SELECT refid FROM reference_address_temp WHERE source LIKE ?)
|
||||||
AND chill_main_address_reference.source LIKE ?
|
AND chill_main_address_reference.source LIKE ?
|
||||||
', [$this->currentSource, $this->currentSource]);
|
', [$this->currentSource, $this->currentSource]);
|
||||||
$this->logger->info(self::LOG_PREFIX.'addresses deleted', ['deleted' => $affected]);
|
$this->logger->info(self::LOG_PREFIX.'addresses deleted', ['deleted' => $affected]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,8 @@ class PostalCodeBaseImporter
|
|||||||
NOW(),
|
NOW(),
|
||||||
NOW()
|
NOW()
|
||||||
FROM g
|
FROM g
|
||||||
ON CONFLICT (code, refpostalcodeid, postalcodeSource) WHERE refpostalcodeid IS NOT NULL DO UPDATE SET label = excluded.label, center = excluded.center, updatedAt = NOW()
|
ON CONFLICT (code, refpostalcodeid, postalcodeSource) WHERE refpostalcodeid IS NOT NULL DO UPDATE
|
||||||
|
SET label = excluded.label, center = excluded.center, updatedAt = CASE WHEN NOT st_equals(excluded.center, chill_main_postal_code.center) OR excluded.label != chill_main_postal_code.label THEN NOW() ELSE chill_main_postal_code.updatedAt END
|
||||||
SQL;
|
SQL;
|
||||||
|
|
||||||
private const VALUE = '(?, ?, ?, ?, ?, ?, ?, ?)';
|
private const VALUE = '(?, ?, ?, ?, ?, ?, ?, ?)';
|
||||||
|
@ -9,7 +9,7 @@ declare(strict_types=1);
|
|||||||
* the LICENSE file that was distributed with this source code.
|
* the LICENSE file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Repository;
|
namespace Chill\MainBundle\Tests\Repository;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\NewsItem;
|
use Chill\MainBundle\Entity\NewsItem;
|
||||||
use Chill\MainBundle\Repository\NewsItemRepository;
|
use Chill\MainBundle\Repository\NewsItemRepository;
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
<?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\Repository;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Repository\NotificationRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class NotificationRepositoryTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
private NotificationRepository $repository;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$this->entityManager = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
$this->repository = new NotificationRepository($this->entityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMarkAllNotificationAsReadForUser(): void
|
||||||
|
{
|
||||||
|
$user = $this->entityManager->createQuery('SELECT u FROM '.User::class.' u')
|
||||||
|
->setMaxResults(1)->getSingleResult();
|
||||||
|
|
||||||
|
$notification = (new Notification())
|
||||||
|
->setRelatedEntityClass('\Dummy')
|
||||||
|
->setRelatedEntityId(0)
|
||||||
|
;
|
||||||
|
$notification->addAddressee($user)->markAsUnreadBy($user);
|
||||||
|
|
||||||
|
$this->entityManager->persist($notification);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$notification->markAsUnreadBy($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->entityManager->refresh($notification);
|
||||||
|
|
||||||
|
if ($notification->isReadBy($user)) {
|
||||||
|
throw new \LogicException('Notification should not be marked as read');
|
||||||
|
}
|
||||||
|
|
||||||
|
$notificationsIds = $this->repository->markAllNotificationAsReadForUser($user);
|
||||||
|
self::assertContains($notification->getId(), $notificationsIds);
|
||||||
|
|
||||||
|
$this->entityManager->clear();
|
||||||
|
|
||||||
|
$notification = $this->entityManager->find(Notification::class, $notification->getId());
|
||||||
|
|
||||||
|
self::assertTrue($notification->isReadBy($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMarkAllNotificationAsUnreadForUser(): void
|
||||||
|
{
|
||||||
|
$user = $this->entityManager->createQuery('SELECT u FROM '.User::class.' u')
|
||||||
|
->setMaxResults(1)->getSingleResult();
|
||||||
|
|
||||||
|
$notification = (new Notification())
|
||||||
|
->setRelatedEntityClass('\Dummy')
|
||||||
|
->setRelatedEntityId(0)
|
||||||
|
;
|
||||||
|
$notification->addAddressee($user); // we do not mark the notification as unread by the user
|
||||||
|
|
||||||
|
$this->entityManager->persist($notification);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$notification->markAsReadBy($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->entityManager->refresh($notification);
|
||||||
|
|
||||||
|
if (!$notification->isReadBy($user)) {
|
||||||
|
throw new \LogicException('Notification should be marked as read');
|
||||||
|
}
|
||||||
|
|
||||||
|
$notificationsIds = $this->repository->markAllNotificationAsUnreadForUser($user, [$notification->getId()]);
|
||||||
|
|
||||||
|
self::assertContains($notification->getId(), $notificationsIds);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
<?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 ChillMainBundle\Tests\Repository;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Repository\UserRepository;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class UserRepositoryTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
private UserRepository $userRepository;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$entityManager = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
$connection = $entityManager->getConnection();
|
||||||
|
$this->userRepository = new UserRepository($entityManager, $connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCountFilteredUsers(): void
|
||||||
|
{
|
||||||
|
self::assertIsInt($this->userRepository->countFilteredUsers(null, ['Active']));
|
||||||
|
self::assertIsInt($this->userRepository->countFilteredUsers(null, ['Active', 'Inactive']));
|
||||||
|
self::assertIsInt($this->userRepository->countFilteredUsers(null, ['Inactive']));
|
||||||
|
self::assertIsInt($this->userRepository->countFilteredUsers('center', ['Active']));
|
||||||
|
self::assertIsInt($this->userRepository->countFilteredUsers('center', ['Active', 'Inactive']));
|
||||||
|
self::assertIsInt($this->userRepository->countFilteredUsers('center', ['Inactive']));
|
||||||
|
self::assertIsInt($this->userRepository->countFilteredUsers('center'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindByFilteredUsers(): void
|
||||||
|
{
|
||||||
|
self::assertIsArray($this->userRepository->findFilteredUsers(null, ['Active']));
|
||||||
|
self::assertIsArray($this->userRepository->findFilteredUsers(null, ['Active', 'Inactive']));
|
||||||
|
self::assertIsArray($this->userRepository->findFilteredUsers(null, ['Inactive']));
|
||||||
|
self::assertIsArray($this->userRepository->findFilteredUsers('center', ['Active']));
|
||||||
|
self::assertIsArray($this->userRepository->findFilteredUsers('center', ['Active', 'Inactive']));
|
||||||
|
self::assertIsArray($this->userRepository->findFilteredUsers('center', ['Inactive']));
|
||||||
|
self::assertIsArray($this->userRepository->findFilteredUsers('center'));
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,8 @@ info:
|
|||||||
title: "Chill api"
|
title: "Chill api"
|
||||||
description: "Api documentation for chill. Currently, work in progress"
|
description: "Api documentation for chill. Currently, work in progress"
|
||||||
servers:
|
servers:
|
||||||
- url: "/api"
|
- url: "/api"
|
||||||
description: "Your current dev server"
|
description: "Your current dev server"
|
||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
@ -165,7 +165,6 @@ components:
|
|||||||
endDate:
|
endDate:
|
||||||
$ref: "#/components/schemas/Date"
|
$ref: "#/components/schemas/Date"
|
||||||
|
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/1.0/search.json:
|
/1.0/search.json:
|
||||||
get:
|
get:
|
||||||
@ -182,25 +181,25 @@ paths:
|
|||||||
The results are ordered by relevance, from the most to the lowest relevant.
|
The results are ordered by relevance, from the most to the lowest relevant.
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
- name: q
|
- name: q
|
||||||
in: query
|
in: query
|
||||||
required: true
|
required: true
|
||||||
description: the pattern to search
|
description: the pattern to search
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
- name: type[]
|
- name: type[]
|
||||||
in: query
|
in: query
|
||||||
required: true
|
required: true
|
||||||
description: the type entities amongst the search is performed
|
description: the type entities amongst the search is performed
|
||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- person
|
- person
|
||||||
- thirdparty
|
- thirdparty
|
||||||
- user
|
- user
|
||||||
- household
|
- household
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "OK"
|
description: "OK"
|
||||||
@ -237,7 +236,7 @@ paths:
|
|||||||
minItems: 2
|
minItems: 2
|
||||||
maxItems: 2
|
maxItems: 2
|
||||||
postcode:
|
postcode:
|
||||||
$ref: '#/components/schemas/PostalCode'
|
$ref: "#/components/schemas/PostalCode"
|
||||||
steps:
|
steps:
|
||||||
type: string
|
type: string
|
||||||
street:
|
street:
|
||||||
@ -261,21 +260,21 @@ paths:
|
|||||||
- address
|
- address
|
||||||
summary: Return an address by id
|
summary: Return an address by id
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The address id
|
description: The address id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Address'
|
$ref: "#/components/schemas/Address"
|
||||||
404:
|
404:
|
||||||
description: "not found"
|
description: "not found"
|
||||||
401:
|
401:
|
||||||
@ -285,14 +284,14 @@ paths:
|
|||||||
- address
|
- address
|
||||||
summary: patch an address
|
summary: patch an address
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The address id
|
description: The address id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@ -321,7 +320,7 @@ paths:
|
|||||||
minItems: 2
|
minItems: 2
|
||||||
maxItems: 2
|
maxItems: 2
|
||||||
postcode:
|
postcode:
|
||||||
$ref: '#/components/schemas/PostalCode'
|
$ref: "#/components/schemas/PostalCode"
|
||||||
steps:
|
steps:
|
||||||
type: string
|
type: string
|
||||||
street:
|
street:
|
||||||
@ -344,28 +343,27 @@ paths:
|
|||||||
400:
|
400:
|
||||||
description: "transition cannot be applyed"
|
description: "transition cannot be applyed"
|
||||||
|
|
||||||
|
|
||||||
/1.0/main/address/{id}/duplicate.json:
|
/1.0/main/address/{id}/duplicate.json:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- address
|
- address
|
||||||
summary: Duplicate an existing address
|
summary: Duplicate an existing address
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The address id that will be duplicated
|
description: The address id that will be duplicated
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Address'
|
$ref: "#/components/schemas/Address"
|
||||||
404:
|
404:
|
||||||
description: "not found"
|
description: "not found"
|
||||||
401:
|
401:
|
||||||
@ -377,12 +375,12 @@ paths:
|
|||||||
- address
|
- address
|
||||||
summary: Return a list of all reference addresses
|
summary: Return a list of all reference addresses
|
||||||
parameters:
|
parameters:
|
||||||
- in: query
|
- in: query
|
||||||
name: postal_code
|
name: postal_code
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
description: The id of a postal code to filter the reference addresses
|
description: The id of a postal code to filter the reference addresses
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
@ -392,21 +390,21 @@ paths:
|
|||||||
- address
|
- address
|
||||||
summary: Return a reference address by id
|
summary: Return a reference address by id
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The reference address id
|
description: The reference address id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AddressReference'
|
$ref: "#/components/schemas/AddressReference"
|
||||||
404:
|
404:
|
||||||
description: "not found"
|
description: "not found"
|
||||||
401:
|
401:
|
||||||
@ -419,27 +417,27 @@ paths:
|
|||||||
- search
|
- search
|
||||||
summary: Return a reference address by id
|
summary: Return a reference address by id
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The reference address id
|
description: The reference address id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
- name: q
|
- name: q
|
||||||
in: query
|
in: query
|
||||||
required: true
|
required: true
|
||||||
description: The search pattern
|
description: The search pattern
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AddressReference'
|
$ref: "#/components/schemas/AddressReference"
|
||||||
404:
|
404:
|
||||||
description: "not found"
|
description: "not found"
|
||||||
401:
|
401:
|
||||||
@ -452,12 +450,12 @@ paths:
|
|||||||
- address
|
- address
|
||||||
summary: Return a list of all postal-code
|
summary: Return a list of all postal-code
|
||||||
parameters:
|
parameters:
|
||||||
- in: query
|
- in: query
|
||||||
name: country
|
name: country
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
description: The id of a country to filter the postal code
|
description: The id of a country to filter the postal code
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
@ -477,7 +475,7 @@ paths:
|
|||||||
code:
|
code:
|
||||||
type: string
|
type: string
|
||||||
country:
|
country:
|
||||||
$ref: '#/components/schemas/Country'
|
$ref: "#/components/schemas/Country"
|
||||||
responses:
|
responses:
|
||||||
401:
|
401:
|
||||||
description: "Unauthorized"
|
description: "Unauthorized"
|
||||||
@ -496,21 +494,21 @@ paths:
|
|||||||
- address
|
- address
|
||||||
summary: Return a postal code by id
|
summary: Return a postal code by id
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The postal code id
|
description: The postal code id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/PostalCode'
|
$ref: "#/components/schemas/PostalCode"
|
||||||
404:
|
404:
|
||||||
description: "not found"
|
description: "not found"
|
||||||
401:
|
401:
|
||||||
@ -523,25 +521,25 @@ paths:
|
|||||||
- search
|
- search
|
||||||
summary: Search a postal code
|
summary: Search a postal code
|
||||||
parameters:
|
parameters:
|
||||||
- name: q
|
- name: q
|
||||||
in: query
|
in: query
|
||||||
required: true
|
required: true
|
||||||
description: The search pattern
|
description: The search pattern
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
- name: country
|
- name: country
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
description: The country id
|
description: The country id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/PostalCode'
|
$ref: "#/components/schemas/PostalCode"
|
||||||
404:
|
404:
|
||||||
description: "not found"
|
description: "not found"
|
||||||
400:
|
400:
|
||||||
@ -561,27 +559,26 @@ paths:
|
|||||||
- address
|
- address
|
||||||
summary: Return a country by id
|
summary: Return a country by id
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The country id
|
description: The country id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Country'
|
$ref: "#/components/schemas/Country"
|
||||||
404:
|
404:
|
||||||
description: "not found"
|
description: "not found"
|
||||||
401:
|
401:
|
||||||
description: "Unauthorized"
|
description: "Unauthorized"
|
||||||
|
|
||||||
|
|
||||||
/1.0/main/user.json:
|
/1.0/main/user.json:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@ -612,21 +609,21 @@ paths:
|
|||||||
- user
|
- user
|
||||||
summary: Return a user by id
|
summary: Return a user by id
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The user id
|
description: The user id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/User'
|
$ref: "#/components/schemas/User"
|
||||||
404:
|
404:
|
||||||
description: "not found"
|
description: "not found"
|
||||||
401:
|
401:
|
||||||
@ -649,14 +646,14 @@ paths:
|
|||||||
- scope
|
- scope
|
||||||
summary: return a list of scopes
|
summary: return a list of scopes
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The scope id
|
description: The scope id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
@ -724,14 +721,14 @@ paths:
|
|||||||
- location
|
- location
|
||||||
summary: Return the given location
|
summary: Return the given location
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The location id
|
description: The location id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "ok"
|
description: "ok"
|
||||||
@ -784,21 +781,21 @@ paths:
|
|||||||
id: 1
|
id: 1
|
||||||
class: 'Chill\PersonBundle\Entity\AccompanyingPeriod'
|
class: 'Chill\PersonBundle\Entity\AccompanyingPeriod'
|
||||||
roles:
|
roles:
|
||||||
- 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE'
|
- "CHILL_PERSON_ACCOMPANYING_PERIOD_SEE"
|
||||||
/1.0/main/notification/{id}/mark/read:
|
/1.0/main/notification/{id}/mark/read:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- notification
|
- notification
|
||||||
summary: mark a notification as read
|
summary: mark a notification as read
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The notification id
|
description: The notification id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
responses:
|
responses:
|
||||||
202:
|
202:
|
||||||
description: "accepted"
|
description: "accepted"
|
||||||
@ -810,23 +807,55 @@ paths:
|
|||||||
- notification
|
- notification
|
||||||
summary: mark a notification as unread
|
summary: mark a notification as unread
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
description: The notification id
|
description: The notification id
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: integer
|
format: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
responses:
|
responses:
|
||||||
202:
|
202:
|
||||||
description: "accepted"
|
description: "accepted"
|
||||||
403:
|
403:
|
||||||
description: "unauthorized"
|
description: "unauthorized"
|
||||||
|
/1.0/main/notification/mark/allread:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- notification
|
||||||
|
summary: Mark all notifications as read
|
||||||
|
responses:
|
||||||
|
202:
|
||||||
|
description: "accepted"
|
||||||
|
403:
|
||||||
|
description: "unauthorized"
|
||||||
|
/1.0/main/notification/mark/undoallread:
|
||||||
|
post: # Use POST method for creating resources
|
||||||
|
tags:
|
||||||
|
- notification
|
||||||
|
summary: Mark notifications as unread
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ids:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
example: [1, 2, 3] # Example array of IDs
|
||||||
|
responses:
|
||||||
|
"202":
|
||||||
|
description: Notifications marked as unread successfully
|
||||||
|
"403":
|
||||||
|
description: Unauthorized
|
||||||
/1.0/main/civility.json:
|
/1.0/main/civility.json:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- civility
|
- civility
|
||||||
summary: Return all civility types
|
summary: Return all civility types
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
@ -844,7 +873,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UserJob'
|
$ref: "#/components/schemas/UserJob"
|
||||||
/1.0/main/workflow/my:
|
/1.0/main/workflow/my:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@ -858,7 +887,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Workflow'
|
$ref: "#/components/schemas/Workflow"
|
||||||
403:
|
403:
|
||||||
description: "Unauthorized"
|
description: "Unauthorized"
|
||||||
/1.0/main/workflow/my-cc:
|
/1.0/main/workflow/my-cc:
|
||||||
@ -874,7 +903,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Workflow'
|
$ref: "#/components/schemas/Workflow"
|
||||||
403:
|
403:
|
||||||
description: "Unauthorized"
|
description: "Unauthorized"
|
||||||
/1.0/main/dashboard-config-item.json:
|
/1.0/main/dashboard-config-item.json:
|
||||||
@ -888,7 +917,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/DashboardConfigItem'
|
$ref: "#/components/schemas/DashboardConfigItem"
|
||||||
403:
|
403:
|
||||||
description: "Unauthorized"
|
description: "Unauthorized"
|
||||||
|
|
||||||
@ -905,6 +934,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/NewsItem'
|
$ref: "#/components/schemas/NewsItem"
|
||||||
403:
|
403:
|
||||||
description: "Unauthorized"
|
description: "Unauthorized"
|
||||||
|
@ -72,6 +72,7 @@ module.exports = function(encore, entries)
|
|||||||
encore.addEntry('mod_blur', __dirname + '/Resources/public/module/blur/index.js');
|
encore.addEntry('mod_blur', __dirname + '/Resources/public/module/blur/index.js');
|
||||||
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');
|
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');
|
||||||
encore.addEntry('mod_notification_toggle_read_status', __dirname + '/Resources/public/module/notification/toggle_read.js');
|
encore.addEntry('mod_notification_toggle_read_status', __dirname + '/Resources/public/module/notification/toggle_read.js');
|
||||||
|
encore.addEntry('mod_notification_toggle_read_all_status', __dirname + '/Resources/public/module/notification/toggle_read_all.ts');
|
||||||
encore.addEntry('mod_pickentity_type', __dirname + '/Resources/public/module/pick-entity/index.js');
|
encore.addEntry('mod_pickentity_type', __dirname + '/Resources/public/module/pick-entity/index.js');
|
||||||
encore.addEntry('mod_entity_workflow_subscribe', __dirname + '/Resources/public/module/entity-workflow-subscribe/index.js');
|
encore.addEntry('mod_entity_workflow_subscribe', __dirname + '/Resources/public/module/entity-workflow-subscribe/index.js');
|
||||||
encore.addEntry('mod_entity_workflow_pick', __dirname + '/Resources/public/module/entity-workflow-pick/index.js');
|
encore.addEntry('mod_entity_workflow_pick', __dirname + '/Resources/public/module/entity-workflow-pick/index.js');
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
<?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\PersonBundle\Export\Filter\PersonFilters;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||||
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
final readonly class WithoutParticipationBetweenDatesFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RollingDateConverterInterface $rollingDateConverter,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function addRole(): ?string
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function alterQuery(QueryBuilder $qb, $data)
|
||||||
|
{
|
||||||
|
$p = 'without_participation_between_dates_filter';
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->andWhere(
|
||||||
|
$qb->expr()->not(
|
||||||
|
$qb->expr()->exists(
|
||||||
|
'SELECT 1 FROM '.AccompanyingPeriodParticipation::class." {$p}_acp JOIN {$p}_acp.accompanyingPeriod {$p}_acpp ".
|
||||||
|
"WHERE {$p}_acp.person = person ".
|
||||||
|
"AND OVERLAPSI({$p}_acp.startDate, {$p}_acp.endDate), (:{$p}_date_after, :{$p}_date_before) = TRUE ".
|
||||||
|
"AND OVERLAPSI({$p}_acpp.openingDate, {$p}_acpp.closingDate), (:{$p}_date_after, :{$p}_date_before) = TRUE"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->setParameter("{$p}_date_after", $this->rollingDateConverter->convert($data['date_after']), Types::DATE_IMMUTABLE)
|
||||||
|
->setParameter("{$p}_date_before", $this->rollingDateConverter->convert($data['date_before']), Types::DATE_IMMUTABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyOn(): string
|
||||||
|
{
|
||||||
|
return Declarations::PERSON_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
|
{
|
||||||
|
$builder->add('date_after', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.filter.person.without_participation_between_dates.date_after',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$builder->add('date_before', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.filter.person.without_participation_between_dates.date_before',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormDefaultData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'date_after' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||||
|
'date_before' => new RollingDate(RollingDate::T_TODAY),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function describeAction($data, $format = 'string')
|
||||||
|
{
|
||||||
|
return ['exports.filter.person.without_participation_between_dates.Filtered by having no participations during period: between', [
|
||||||
|
'dateafter' => $this->rollingDateConverter->convert($data['date_after']),
|
||||||
|
'datebefore' => $this->rollingDateConverter->convert($data['date_before']),
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle()
|
||||||
|
{
|
||||||
|
return 'export.filter.person.without_participation_between_dates.title';
|
||||||
|
}
|
||||||
|
}
|
@ -85,11 +85,11 @@ final readonly class AccompanyingPeriodWorkEndDateBetweenDateFilter implements F
|
|||||||
};
|
};
|
||||||
$end = match ($data['keep_null']) {
|
$end = match ($data['keep_null']) {
|
||||||
true => $qb->expr()->orX(
|
true => $qb->expr()->orX(
|
||||||
$qb->expr()->gt('acpw.endDate', ':'.$as),
|
$qb->expr()->gt('COALESCE(acp.closingDate, acpw.endDate)', ':'.$as),
|
||||||
$qb->expr()->isNull('acpw.endDate')
|
$qb->expr()->isNull('acpw.endDate')
|
||||||
),
|
),
|
||||||
false => $qb->expr()->andX(
|
false => $qb->expr()->andX(
|
||||||
$qb->expr()->gt('acpw.endDate', ':'.$as),
|
$qb->expr()->gt('COALESCE(acp.closingDate, acpw.endDate)', ':'.$as),
|
||||||
$qb->expr()->isNotNull('acpw.endDate')
|
$qb->expr()->isNotNull('acpw.endDate')
|
||||||
),
|
),
|
||||||
default => throw new \LogicException('This value is not supported'),
|
default => throw new \LogicException('This value is not supported'),
|
||||||
|
@ -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 Export\Filter\PersonFilters;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Chill\MainBundle\Test\Export\AbstractFilterTest;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Export\Filter\PersonFilters\WithoutParticipationBetweenDatesFilter;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class WithoutParticipationBetweenDatesFilterTest extends AbstractFilterTest
|
||||||
|
{
|
||||||
|
private WithoutParticipationBetweenDatesFilter $filter;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
|
||||||
|
$this->filter = self::getContainer()->get(WithoutParticipationBetweenDatesFilter::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilter()
|
||||||
|
{
|
||||||
|
return $this->filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getFormData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'date_after' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||||
|
'date_before' => new RollingDate(RollingDate::T_TODAY),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getQueryBuilders(): iterable
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$em = self::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
|
||||||
|
return [
|
||||||
|
$em->createQueryBuilder()
|
||||||
|
->select('person.id')
|
||||||
|
->from(Person::class, 'person'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -124,6 +124,10 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: chill.export_filter, alias: person_with_participation_between_dates_filter }
|
- { name: chill.export_filter, alias: person_with_participation_between_dates_filter }
|
||||||
|
|
||||||
|
Chill\PersonBundle\Export\Filter\PersonFilters\WithoutParticipationBetweenDatesFilter:
|
||||||
|
tags:
|
||||||
|
- { name: chill.export_filter, alias: person_without_participation_between_dates_filter }
|
||||||
|
|
||||||
## Aggregators
|
## Aggregators
|
||||||
chill.person.export.aggregator_nationality:
|
chill.person.export.aggregator_nationality:
|
||||||
class: Chill\PersonBundle\Export\Aggregator\PersonAggregators\NationalityAggregator
|
class: Chill\PersonBundle\Export\Aggregator\PersonAggregators\NationalityAggregator
|
||||||
|
@ -136,6 +136,9 @@ exports:
|
|||||||
Filtered by person\'s geographical unit (based on address) computed at date, only units:
|
Filtered by person\'s geographical unit (based on address) computed at date, only units:
|
||||||
"Filtré par zone géographique sur base de l'adresse, calculé à {datecalc, date, short}, seulement les zones suivantes: {units}"
|
"Filtré par zone géographique sur base de l'adresse, calculé à {datecalc, date, short}, seulement les zones suivantes: {units}"
|
||||||
filter:
|
filter:
|
||||||
|
person:
|
||||||
|
without_participation_between_dates:
|
||||||
|
"Filtered by having no participations during period: between": "Uniquement les usagers qui n'ont été concerné par aucun parcours entre le {dateafter, date, short} et le {datebefore, date, short}"
|
||||||
course:
|
course:
|
||||||
not_having_address_reference:
|
not_having_address_reference:
|
||||||
describe: >-
|
describe: >-
|
||||||
|
@ -1188,6 +1188,10 @@ export:
|
|||||||
date_before: Concerné par un parcours avant le
|
date_before: Concerné par un parcours avant le
|
||||||
title: Filtrer les usagers ayant été associés à un parcours ouverts un jour dans la période de temps indiquée
|
title: Filtrer les usagers ayant été associés à un parcours ouverts un jour dans la période de temps indiquée
|
||||||
'Filtered by participations during period: between %dateafter% and %datebefore%': 'Filtré par personne concerné par un parcours dans la periode entre: %dateafter% et %datebefore%'
|
'Filtered by participations during period: between %dateafter% and %datebefore%': 'Filtré par personne concerné par un parcours dans la periode entre: %dateafter% et %datebefore%'
|
||||||
|
without_participation_between_dates:
|
||||||
|
date_after: Après le
|
||||||
|
date_before: Avant le
|
||||||
|
title: Filtrer les usagers n'ayant été associés à aucun parcours
|
||||||
|
|
||||||
course:
|
course:
|
||||||
not_having_address_reference:
|
not_having_address_reference:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user