mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-28 17:44:58 +00:00
Compare commits
6 Commits
master-202
...
295-cancel
Author | SHA1 | Date | |
---|---|---|---|
fad7bdf235
|
|||
8521cea46c
|
|||
4ead7ba761
|
|||
9721b166eb
|
|||
1b21cd6c33
|
|||
97860a9487
|
@@ -1,6 +0,0 @@
|
|||||||
kind: Fixed
|
|
||||||
body: Show only the current referrer in the page "show" for an accompanying period
|
|
||||||
workf
|
|
||||||
time: 2024-09-16T15:18:43.017401122+02:00
|
|
||||||
custom:
|
|
||||||
Issue: "308"
|
|
@@ -1,6 +0,0 @@
|
|||||||
kind: Fixed
|
|
||||||
body: |
|
|
||||||
Correctly compute the grouping by referrer aggregator
|
|
||||||
time: 2024-09-16T15:51:50.268336979+02:00
|
|
||||||
custom:
|
|
||||||
Issue: "309"
|
|
@@ -1,4 +1,4 @@
|
|||||||
## v2.23.0 - 2024-07-23 & 2024-07-19
|
## v2.23.0 - 2024-07-23
|
||||||
### 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,25 +6,6 @@
|
|||||||
|
|
||||||
* 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.
|
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
## 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.
|
|
@@ -1,3 +0,0 @@
|
|||||||
## 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.
|
|
2
.env
2
.env
@@ -23,7 +23,7 @@ TRUSTED_HOSTS='^(localhost|example\.com|nginx)$'
|
|||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
## Wopi server for editing documents online
|
## Wopi server for editing documents online
|
||||||
EDITOR_SERVER=http://collabora:9980
|
WOPI_SERVER=http://collabora:9980
|
||||||
|
|
||||||
# must be manually set in .env.local
|
# must be manually set in .env.local
|
||||||
# ADMIN_PASSWORD=
|
# ADMIN_PASSWORD=
|
||||||
|
@@ -41,5 +41,3 @@ DATABASE_URL="postgresql://postgres:postgres@db:5432/test?serverVersion=14&chars
|
|||||||
ASYNC_UPLOAD_TEMP_URL_KEY=
|
ASYNC_UPLOAD_TEMP_URL_KEY=
|
||||||
ASYNC_UPLOAD_TEMP_URL_BASE_PATH=
|
ASYNC_UPLOAD_TEMP_URL_BASE_PATH=
|
||||||
ASYNC_UPLOAD_TEMP_URL_CONTAINER=
|
ASYNC_UPLOAD_TEMP_URL_CONTAINER=
|
||||||
|
|
||||||
EDITOR_SERVER=https://localhost:9980
|
|
||||||
|
31
CHANGELOG.md
31
CHANGELOG.md
@@ -6,43 +6,14 @@ 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.24.0 - 2024-09-11
|
## v2.23.0 - 2024-07-23
|
||||||
### 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,7 +43,6 @@
|
|||||||
"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,12 +39,9 @@ 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
|
||||||
@@ -59,7 +56,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 = $clock->now();
|
$now = new DateTimeImmutable('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)
|
||||||
@@ -72,14 +69,9 @@ Implements a :code:`Chill\MainBundle\Cron\CronJobInterface`. Here is an example:
|
|||||||
return 'arbitrary-and-unique-key';
|
return 'arbitrary-and-unique-key';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(array $lastExecutionData): void
|
public function run(): 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];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -55,7 +55,7 @@
|
|||||||
"mime": "^4.0.0",
|
"mime": "^4.0.0",
|
||||||
"pdfjs-dist": "^4.3.136",
|
"pdfjs-dist": "^4.3.136",
|
||||||
"vis-network": "^9.1.0",
|
"vis-network": "^9.1.0",
|
||||||
"vue": "^3.5.6",
|
"vue": "^3.2.37",
|
||||||
"vue-i18n": "^9.1.6",
|
"vue-i18n": "^9.1.6",
|
||||||
"vue-multiselect": "3.0.0-alpha.2",
|
"vue-multiselect": "3.0.0-alpha.2",
|
||||||
"vue-toast-notification": "^3.1.2",
|
"vue-toast-notification": "^3.1.2",
|
||||||
|
@@ -1,99 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\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,7 +19,6 @@ 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;
|
||||||
@@ -45,7 +44,6 @@ 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;
|
||||||
|
|
||||||
@@ -191,26 +189,19 @@ 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 no fields are present
|
// throw an error if any fields are present
|
||||||
if (!\array_key_exists('fields', $data)) {
|
if (!\array_key_exists('fields', $data)) {
|
||||||
throw new InvalidArgumentException('No fields have been checked.');
|
throw new InvalidArgumentException('Any 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', 'person')
|
->join('activity.person', 'actperson');
|
||||||
->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('person.centerHistory', 'centerHistory');
|
$qb->join('actperson.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'),
|
||||||
@@ -233,22 +224,17 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'person_firstname':
|
case 'person_firstname':
|
||||||
$qb->addSelect('person.firstName AS person_firstname');
|
$qb->addSelect('actperson.firstName AS person_firstname');
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'person_lastname':
|
case 'person_lastname':
|
||||||
$qb->addSelect('person.lastName AS person_lastname');
|
$qb->addSelect('actperson.lastName AS person_lastname');
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'person_id':
|
case 'person_id':
|
||||||
$qb->addSelect('person.id AS person_id');
|
$qb->addSelect('actperson.id AS person_id');
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'household_id':
|
|
||||||
$qb->addSelect('household.id AS household_id');
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -298,7 +284,7 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
|||||||
return ActivityStatsVoter::LISTS;
|
return ActivityStatsVoter::LISTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supportsModifiers(): array
|
public function supportsModifiers()
|
||||||
{
|
{
|
||||||
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 = acp"
|
'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = activity.accompanyingPeriod"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -39,7 +39,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function alterQuery(QueryBuilder $qb, $data): void
|
public function alterQuery(QueryBuilder $qb, $data)
|
||||||
{
|
{
|
||||||
// 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'): array
|
public function describeAction($data, $format = 'string')
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
[] === $data['reasons'] ?
|
[] === $data['reasons'] ?
|
||||||
@@ -141,7 +141,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTitle(): string
|
public function getTitle()
|
||||||
{
|
{
|
||||||
return 'export.filter.activity.person_between_dates.title';
|
return 'export.filter.activity.person_between_dates.title';
|
||||||
}
|
}
|
||||||
|
@@ -243,7 +243,3 @@ 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,9 +428,6 @@ 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,15 +14,10 @@
|
|||||||
></vue-multiselect>
|
></vue-multiselect>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="display-options row justify-content-between" style="margin-top: 1rem;">
|
||||||
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"
|
<label class="input-group-text" for="slotDuration">Durée des créneaux</label>
|
||||||
>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>
|
||||||
@@ -63,20 +58,13 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-3">
|
<div class="col-sm-3 col-xs-12">
|
||||||
<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
|
<input id="showHideWE" class="mt-0" type="checkbox" v-model="showWeekends">
|
||||||
id="showHideWE"
|
|
||||||
class="mt-0"
|
|
||||||
type="checkbox"
|
|
||||||
v-model="showWeekends"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
<label for="showHideWE" class="form-check-label input-group-text"
|
<label for="showHideWE" class="form-check-label input-group-text">Week-ends</label>
|
||||||
>Week-ends</label
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,86 +72,39 @@
|
|||||||
<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'">{{
|
<b v-if="arg.event.extendedProps.is === 'remote'">{{ arg.event.title}}</b>
|
||||||
arg.event.title
|
<b v-else-if="arg.event.extendedProps.is === 'range'">{{ arg.timeText }} - {{ arg.event.extendedProps.locationName }}</b>
|
||||||
}}</b>
|
<b v-else-if="arg.event.extendedProps.is === 'local'">{{ arg.event.title}}</b>
|
||||||
<b v-else-if="arg.event.extendedProps.is === 'range'"
|
|
||||||
>{{ arg.timeText }} - {{ arg.event.extendedProps.locationName }}</b
|
|
||||||
>
|
|
||||||
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
|
|
||||||
arg.event.title
|
|
||||||
}}</b>
|
|
||||||
<b v-else >no 'is'</b>
|
<b v-else >no 'is'</b>
|
||||||
<a
|
<a v-if="arg.event.extendedProps.is === 'range'" class="fa fa-fw fa-times delete"
|
||||||
v-if="arg.event.extendedProps.is === 'range'"
|
@click.prevent="onClickDelete(arg.event)">
|
||||||
class="fa fa-fw fa-times delete"
|
|
||||||
@click.prevent="onClickDelete(arg.event)"
|
|
||||||
>
|
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</FullCalendar>
|
</FullCalendar>
|
||||||
|
|
||||||
<div id="copy-widget">
|
<div id="copy-widget">
|
||||||
<div class="container mt-2 mb-2">
|
<div class="container">
|
||||||
|
<div class="row align-items-center">
|
||||||
<div class="row justify-content-between align-items-center mb-4">
|
<div class="col-sm-4 col-xs-12">
|
||||||
<div class="col-xs-12 col-sm-3 col-md-2">
|
<h6 class="chill-red">{{ $t('copy_range_from_to') }}</h6>
|
||||||
<h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-9 col-md-2">
|
<div class="col-sm-3 col-xs-12">
|
||||||
<select v-model="dayOrWeek" id="dayOrWeek" class="form-select">
|
|
||||||
<option value="day">{{ $t("from_day_to_day") }}</option>
|
|
||||||
<option value="week">{{ $t("from_week_to_week") }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<template v-if="dayOrWeek === 'day'">
|
|
||||||
<div class="col-xs-12 col-sm-3 col-md-3">
|
|
||||||
<input class="form-control" type="date" v-model="copyFrom" />
|
<input class="form-control" type="date" v-model="copyFrom" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron">
|
<div class="col-sm-1 col-xs-12" style="text-align: center; font-size: x-large;">
|
||||||
<i class="fa fa-angle-double-right"></i>
|
<i class="fa fa-angle-double-right"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-3 col-md-3">
|
<div class="col-sm-3 col-xs-12" >
|
||||||
<input class="form-control" type="date" v-model="copyTo" />
|
<input class="form-control" type="date" v-model="copyTo" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-5 col-md-1">
|
<div class="col-sm-1">
|
||||||
<button class="btn btn-action float-end" @click="copyDay">
|
<button class="btn btn-action" @click="copyDay">
|
||||||
{{ $t("copy_range") }}
|
{{ $t('copy_range') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<!-- not directly seen, but include in a modal -->
|
<!-- not directly seen, but include in a modal -->
|
||||||
@@ -171,31 +112,21 @@
|
|||||||
</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, onMounted } from "vue";
|
import {reactive, computed, ref} 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, {
|
import interactionPlugin, {DropArg, EventResizeDoneArg} from "@fullcalendar/interaction";
|
||||||
DropArg,
|
|
||||||
EventResizeDoneArg,
|
|
||||||
} from "@fullcalendar/interaction";
|
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import {
|
import {EventApi, DateSelectArg, EventDropArg, EventClickArg} from "@fullcalendar/core";
|
||||||
EventApi,
|
import {ISOToDate} from "../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||||
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";
|
||||||
@@ -206,60 +137,17 @@ const store = useStore(key);
|
|||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
const showWeekends = ref(false);
|
const showWeekends = ref(false);
|
||||||
const slotDuration = ref("00:15:00");
|
const slotDuration = ref('00:05: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,
|
||||||
@@ -276,9 +164,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'
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -292,23 +180,20 @@ const locations = computed<Location[]>(() => {
|
|||||||
|
|
||||||
const pickedLocation = computed<Location | null>({
|
const pickedLocation = computed<Location | null>({
|
||||||
get(): Location | null {
|
get(): Location | null {
|
||||||
return (
|
return store.state.locations.locationPicked || store.state.locations.currentLocation;
|
||||||
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
|
||||||
@@ -345,60 +230,51 @@ 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", {
|
store.dispatch('fullCalendar/setCurrentDatesView', {start: event.start, end: event.end});
|
||||||
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(
|
window.alert("Indiquez une localisation avant de créer une période de disponibilité.");
|
||||||
"Indiquez une localisation avant de créer une période de disponibilité."
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
store.dispatch("calendarRanges/createRange", {
|
store.dispatch('calendarRanges/createRange', {start: event.start, end: event.end, location: pickedLocation.value});
|
||||||
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 {
|
||||||
if (event.extendedProps.is !== "range") {
|
console.log('onClickDelete', event);
|
||||||
|
|
||||||
|
if (event.extendedProps.is !== 'range') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
store.dispatch(
|
store.dispatch('calendarRanges/deleteRange', event.extendedProps.calendarRangeId);
|
||||||
"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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,26 +285,10 @@ function copyDay() {
|
|||||||
if (null === copyFrom.value || null === copyTo.value) {
|
if (null === copyFrom.value || null === copyTo.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
store.dispatch("calendarRanges/copyFromDayToAnotherDay", {
|
|
||||||
from: ISOToDate(copyFrom.value),
|
store.dispatch('calendarRanges/copyFromDayToAnotherDay', {from: ISOToDate(copyFrom.value), to: ISOToDate(copyTo.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>
|
||||||
@@ -439,9 +299,4 @@ onMounted(() => {
|
|||||||
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,9 +5,11 @@ 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",
|
copy_range_from_to: "Copier les plages d'un jour à l'autre",
|
||||||
from_day_to_day: "d'un jour à l'autre",
|
copy_range_to_next_day: "Copier les plages du jour au jour suivant",
|
||||||
from_week_to_week: "d'une semaine à l'autre",
|
copy_range_from_day: "Copier les plages du ",
|
||||||
|
to_the_next_day: " au jour suivant",
|
||||||
|
copy_range_to_next_week: "Copier les plages de la semaine à la semaine suivante",
|
||||||
copy_range_how_to: "Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.",
|
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,23 +52,6 @@ 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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -255,7 +238,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);
|
||||||
@@ -263,23 +246,6 @@ 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -201,4 +201,36 @@ class DocumentPersonController extends AbstractController
|
|||||||
['document' => $document, 'person' => $person]
|
['document' => $document, 'person' => $person]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/{id}/signature', name: 'person_document_signature', methods: 'GET')]
|
||||||
|
public function signature(Person $person, PersonDocument $document): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
|
||||||
|
$this->denyAccessUnlessGranted('CHILL_PERSON_DOCUMENT_SEE', $document);
|
||||||
|
|
||||||
|
$event = new PrivacyEvent($person, [
|
||||||
|
'element_class' => PersonDocument::class,
|
||||||
|
'element_id' => $document->getId(),
|
||||||
|
'action' => 'show',
|
||||||
|
]);
|
||||||
|
$this->eventDispatcher->dispatch($event, PrivacyEvent::PERSON_PRIVACY_EVENT);
|
||||||
|
|
||||||
|
$storedObject = $document->getObject();
|
||||||
|
$content = $this->storedObjectManagerInterface->read($storedObject);
|
||||||
|
$zones = $this->PDFSignatureZoneParser->findSignatureZones($content);
|
||||||
|
|
||||||
|
$signature = [];
|
||||||
|
$signature['id'] = 1;
|
||||||
|
$signature['storedObject'] = [ // TEMP
|
||||||
|
'filename' => $storedObject->getFilename(),
|
||||||
|
'iv' => $storedObject->getIv(),
|
||||||
|
'keyInfos' => $storedObject->getKeyInfos(),
|
||||||
|
];
|
||||||
|
$signature['zones'] = $zones;
|
||||||
|
|
||||||
|
return $this->render(
|
||||||
|
'@ChillDocStore/PersonDocument/signature.html.twig',
|
||||||
|
['document' => $document, 'person' => $person, 'signature' => $signature]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,19 +15,12 @@ use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessa
|
|||||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
|
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
|
||||||
|
|
||||||
class SignatureRequestController
|
class SignatureRequestController
|
||||||
{
|
{
|
||||||
@@ -35,24 +28,12 @@ class SignatureRequestController
|
|||||||
private readonly MessageBusInterface $messageBus,
|
private readonly MessageBusInterface $messageBus,
|
||||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||||
private readonly ChillEntityRenderManagerInterface $entityRender,
|
|
||||||
private readonly NormalizerInterface $normalizer,
|
|
||||||
private readonly Security $security,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
|
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
|
||||||
public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse
|
public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!$this->security->isGranted(EntityWorkflowStepSignatureVoter::SIGN, $signature)) {
|
|
||||||
throw new AccessDeniedHttpException('not authorized to sign this step');
|
|
||||||
}
|
|
||||||
|
|
||||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||||
|
|
||||||
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
|
|
||||||
return new JsonResponse([], status: Response::HTTP_CONFLICT);
|
|
||||||
}
|
|
||||||
|
|
||||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||||
$content = $this->storedObjectManager->read($storedObject);
|
$content = $this->storedObjectManager->read($storedObject);
|
||||||
|
|
||||||
@@ -70,14 +51,8 @@ class SignatureRequestController
|
|||||||
$signature->getId(),
|
$signature->getId(),
|
||||||
$zone,
|
$zone,
|
||||||
$data['zone']['index'],
|
$data['zone']['index'],
|
||||||
'Signed by IP: '.(string) $request->getClientIp().', authenticated user: '.$this->entityRender->renderString($this->security->getUser(), []),
|
'test signature', // reason (string)
|
||||||
$this->entityRender->renderString($signature->getSigner(), [
|
'Mme Caroline Diallo', // signerText (string)
|
||||||
// options for user render
|
|
||||||
'absence' => false,
|
|
||||||
'main_scope' => false,
|
|
||||||
// options for person render
|
|
||||||
'addAge' => false,
|
|
||||||
]),
|
|
||||||
$content
|
$content
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -87,16 +62,6 @@ class SignatureRequestController
|
|||||||
#[Route('/api/1.0/document/workflow/{id}/check-signature', name: 'chill_docstore_check_signature')]
|
#[Route('/api/1.0/document/workflow/{id}/check-signature', name: 'chill_docstore_check_signature')]
|
||||||
public function checkSignature(EntityWorkflowStepSignature $signature): JsonResponse
|
public function checkSignature(EntityWorkflowStepSignature $signature): JsonResponse
|
||||||
{
|
{
|
||||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
return new JsonResponse($signature->getState(), JsonResponse::HTTP_OK, []);
|
||||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
|
||||||
|
|
||||||
return new JsonResponse(
|
|
||||||
[
|
|
||||||
'state' => $signature->getState(),
|
|
||||||
'storedObject' => $this->normalizer->normalize($storedObject, 'json'),
|
|
||||||
],
|
|
||||||
JsonResponse::HTTP_OK,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,47 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Controller;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
|
||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
|
||||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectRestoreInterface;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
|
||||||
|
|
||||||
final readonly class StoredObjectRestoreVersionApiController
|
|
||||||
{
|
|
||||||
public function __construct(private Security $security, private StoredObjectRestoreInterface $storedObjectRestore, private EntityManagerInterface $entityManager, private SerializerInterface $serializer) {}
|
|
||||||
|
|
||||||
#[Route('/api/1.0/doc-store/stored-object/restore-from-version/{id}', methods: ['POST'])]
|
|
||||||
public function restoreStoredObjectVersion(StoredObjectVersion $storedObjectVersion): JsonResponse
|
|
||||||
{
|
|
||||||
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObjectVersion->getStoredObject())) {
|
|
||||||
throw new AccessDeniedHttpException('not allowed to edit the stored object');
|
|
||||||
}
|
|
||||||
|
|
||||||
$newVersion = $this->storedObjectRestore->restore($storedObjectVersion);
|
|
||||||
|
|
||||||
$this->entityManager->persist($newVersion);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
return new JsonResponse(
|
|
||||||
$this->serializer->serialize($newVersion, 'json', [AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]),
|
|
||||||
json: true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,69 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Controller;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
|
||||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
|
||||||
use Chill\MainBundle\Serializer\Model\Collection;
|
|
||||||
use Doctrine\Common\Collections\Criteria;
|
|
||||||
use Doctrine\Common\Collections\Order;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
|
||||||
|
|
||||||
final readonly class StoredObjectVersionApiController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private PaginatorFactoryInterface $paginatorFactory,
|
|
||||||
private SerializerInterface $serializer,
|
|
||||||
private Security $security,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists the versions of the specified stored object.
|
|
||||||
*
|
|
||||||
* @param StoredObject $storedObject the stored object whose versions are to be listed
|
|
||||||
*
|
|
||||||
* @return JsonResponse a JSON response containing the serialized versions of the stored object, encapsulated in a collection
|
|
||||||
*
|
|
||||||
* @throws AccessDeniedHttpException if the user is not allowed to see the stored object
|
|
||||||
*/
|
|
||||||
#[Route('/api/1.0/doc-store/stored-object/{uuid}/versions', name: 'chill_doc_store_stored_object_versions_list')]
|
|
||||||
public function listVersions(StoredObject $storedObject): JsonResponse
|
|
||||||
{
|
|
||||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
|
||||||
throw new AccessDeniedHttpException('not allowed to see this stored object');
|
|
||||||
}
|
|
||||||
|
|
||||||
$total = $storedObject->getVersions()->count();
|
|
||||||
$paginator = $this->paginatorFactory->create($total);
|
|
||||||
|
|
||||||
$criteria = Criteria::create();
|
|
||||||
$criteria->orderBy(['id' => Order::Ascending]);
|
|
||||||
$criteria->setMaxResults($paginator->getItemsPerPage())->setFirstResult($paginator->getCurrentPageFirstItemNumber());
|
|
||||||
$items = $storedObject->getVersions()->matching($criteria);
|
|
||||||
|
|
||||||
return new JsonResponse(
|
|
||||||
$this->serializer->serialize(
|
|
||||||
new Collection($items, $paginator),
|
|
||||||
'json',
|
|
||||||
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
|
|
||||||
),
|
|
||||||
json: true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -18,7 +18,6 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
|||||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\Common\Collections\Selectable;
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Ramsey\Uuid\Uuid;
|
use Ramsey\Uuid\Uuid;
|
||||||
use Ramsey\Uuid\UuidInterface;
|
use Ramsey\Uuid\UuidInterface;
|
||||||
@@ -90,10 +89,10 @@ class StoredObject implements Document, TrackCreationInterface
|
|||||||
private string $generationErrors = '';
|
private string $generationErrors = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, StoredObjectVersion>&Selectable<int, StoredObjectVersion>
|
* @var Collection<int, StoredObjectVersion>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true)]
|
||||||
private Collection&Selectable $versions;
|
private Collection $versions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param StoredObject::STATUS_* $status
|
* @param StoredObject::STATUS_* $status
|
||||||
@@ -257,7 +256,7 @@ class StoredObject implements Document, TrackCreationInterface
|
|||||||
return $this->template;
|
return $this->template;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getVersions(): Collection&Selectable
|
public function getVersions(): Collection
|
||||||
{
|
{
|
||||||
return $this->versions;
|
return $this->versions;
|
||||||
}
|
}
|
||||||
|
@@ -37,7 +37,7 @@ class StoredObjectPointInTime implements TrackCreationInterface
|
|||||||
#[ORM\ManyToOne(targetEntity: StoredObjectVersion::class, inversedBy: 'pointInTimes')]
|
#[ORM\ManyToOne(targetEntity: StoredObjectVersion::class, inversedBy: 'pointInTimes')]
|
||||||
#[ORM\JoinColumn(name: 'stored_object_version_id', nullable: false)]
|
#[ORM\JoinColumn(name: 'stored_object_version_id', nullable: false)]
|
||||||
private StoredObjectVersion $objectVersion,
|
private StoredObjectVersion $objectVersion,
|
||||||
#[ORM\Column(name: 'reason', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, enumType: StoredObjectPointInTimeReasonEnum::class)]
|
#[ORM\Column(name: 'reason', type: 'text', nullable: false, enumType: StoredObjectPointInTimeReasonEnum::class)]
|
||||||
private StoredObjectPointInTimeReasonEnum $reason,
|
private StoredObjectPointInTimeReasonEnum $reason,
|
||||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
private ?User $byUser = null,
|
private ?User $byUser = null,
|
||||||
|
@@ -48,25 +48,6 @@ class StoredObjectVersion implements TrackCreationInterface
|
|||||||
#[ORM\OneToMany(mappedBy: 'objectVersion', targetEntity: StoredObjectPointInTime::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'objectVersion', targetEntity: StoredObjectPointInTime::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||||
private Collection&Selectable $pointInTimes;
|
private Collection&Selectable $pointInTimes;
|
||||||
|
|
||||||
/**
|
|
||||||
* Previous storedObjectVersion, from which the current stored object version is created.
|
|
||||||
*
|
|
||||||
* If null, the current stored object version is generated by other means.
|
|
||||||
*
|
|
||||||
* Those version may be associated with the same storedObject, or not. In this last case, that means that
|
|
||||||
* the stored object's current version is created from another stored object version.
|
|
||||||
*/
|
|
||||||
#[ORM\ManyToOne(targetEntity: StoredObjectVersion::class)]
|
|
||||||
private ?StoredObjectVersion $createdFrom = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of stored object versions created from the current version.
|
|
||||||
*
|
|
||||||
* @var Collection<int, StoredObjectVersion>
|
|
||||||
*/
|
|
||||||
#[ORM\OneToMany(mappedBy: 'createdFrom', targetEntity: StoredObjectVersion::class)]
|
|
||||||
private Collection $children;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
/**
|
/**
|
||||||
* The stored object associated with this version.
|
* The stored object associated with this version.
|
||||||
@@ -106,7 +87,6 @@ class StoredObjectVersion implements TrackCreationInterface
|
|||||||
) {
|
) {
|
||||||
$this->filename = $filename ?? self::generateFilename($this);
|
$this->filename = $filename ?? self::generateFilename($this);
|
||||||
$this->pointInTimes = new ArrayCollection();
|
$this->pointInTimes = new ArrayCollection();
|
||||||
$this->children = new ArrayCollection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function generateFilename(StoredObjectVersion $storedObjectVersion): string
|
public static function generateFilename(StoredObjectVersion $storedObjectVersion): string
|
||||||
@@ -169,6 +149,8 @@ class StoredObjectVersion implements TrackCreationInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @return $this
|
||||||
|
*
|
||||||
* @internal use @see{StoredObjectPointInTime} constructor instead
|
* @internal use @see{StoredObjectPointInTime} constructor instead
|
||||||
*/
|
*/
|
||||||
public function addPointInTime(StoredObjectPointInTime $storedObjectPointInTime): self
|
public function addPointInTime(StoredObjectPointInTime $storedObjectPointInTime): self
|
||||||
@@ -188,42 +170,4 @@ class StoredObjectVersion implements TrackCreationInterface
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedFrom(): ?StoredObjectVersion
|
|
||||||
{
|
|
||||||
return $this->createdFrom;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setCreatedFrom(?StoredObjectVersion $createdFrom): StoredObjectVersion
|
|
||||||
{
|
|
||||||
if (null === $createdFrom && null !== $this->createdFrom) {
|
|
||||||
$this->createdFrom->removeChild($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
$createdFrom?->addChild($this);
|
|
||||||
|
|
||||||
$this->createdFrom = $createdFrom;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addChild(StoredObjectVersion $child): self
|
|
||||||
{
|
|
||||||
if (!$this->children->contains($child)) {
|
|
||||||
$this->children->add($child);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function removeChild(StoredObjectVersion $child): self
|
|
||||||
{
|
|
||||||
$result = $this->children->removeElement($child);
|
|
||||||
|
|
||||||
if (false === $result) {
|
|
||||||
throw new \UnexpectedValueException('the child is not associated with the current stored object version');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,6 @@ import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.v
|
|||||||
import {createApp} from "vue";
|
import {createApp} from "vue";
|
||||||
import {StoredObject, StoredObjectStatusChange} from "../../types";
|
import {StoredObject, StoredObjectStatusChange} from "../../types";
|
||||||
import {is_object_ready} from "../../vuejs/StoredObjectButton/helpers";
|
import {is_object_ready} from "../../vuejs/StoredObjectButton/helpers";
|
||||||
import ToastPlugin from "vue-toast-notification";
|
|
||||||
|
|
||||||
const i18n = _createI18n({});
|
const i18n = _createI18n({});
|
||||||
|
|
||||||
@@ -49,6 +48,6 @@ window.addEventListener('DOMContentLoaded', function (e) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(i18n).use(ToastPlugin).mount(el);
|
app.use(i18n).mount(el);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
@@ -1,130 +1,100 @@
|
|||||||
import {
|
import {DateTime, User} from "../../../ChillMainBundle/Resources/public/types";
|
||||||
DateTime,
|
|
||||||
User,
|
|
||||||
} from "../../../ChillMainBundle/Resources/public/types";
|
|
||||||
|
|
||||||
export type StoredObjectStatus = "empty"|"ready"|"failure"|"pending";
|
export type StoredObjectStatus = "empty"|"ready"|"failure"|"pending";
|
||||||
|
|
||||||
export interface StoredObject {
|
export interface StoredObject {
|
||||||
id: number;
|
id: number,
|
||||||
title: string | null;
|
title: string|null,
|
||||||
uuid: string;
|
uuid: string,
|
||||||
prefix: string;
|
prefix: string,
|
||||||
status: StoredObjectStatus;
|
status: StoredObjectStatus,
|
||||||
currentVersion:
|
currentVersion: null|StoredObjectVersionCreated|StoredObjectVersionPersisted,
|
||||||
| null
|
totalVersions: number,
|
||||||
| StoredObjectVersionCreated
|
datas: object,
|
||||||
| StoredObjectVersionPersisted;
|
|
||||||
totalVersions: number;
|
|
||||||
datas: object;
|
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
creationDate: DateTime;
|
creationDate: DateTime,
|
||||||
createdAt: DateTime | null;
|
createdAt: DateTime|null,
|
||||||
createdBy: User | null;
|
createdBy: User|null,
|
||||||
_permissions: {
|
_permissions: {
|
||||||
canEdit: boolean;
|
canEdit: boolean,
|
||||||
canSee: boolean;
|
canSee: boolean,
|
||||||
};
|
},
|
||||||
_links?: {
|
_links?: {
|
||||||
dav_link?: {
|
dav_link?: {
|
||||||
href: string;
|
href: string
|
||||||
expiration: number;
|
expiration: number
|
||||||
};
|
},
|
||||||
};
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoredObjectVersion {
|
export interface StoredObjectVersion {
|
||||||
/**
|
/**
|
||||||
* filename of the object in the object storage
|
* filename of the object in the object storage
|
||||||
*/
|
*/
|
||||||
filename: string;
|
filename: string,
|
||||||
iv: number[];
|
iv: number[],
|
||||||
keyInfos: JsonWebKey;
|
keyInfos: JsonWebKey,
|
||||||
type: string;
|
type: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoredObjectVersionCreated extends StoredObjectVersion {
|
export interface StoredObjectVersionCreated extends StoredObjectVersion {
|
||||||
persisted: false;
|
persisted: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoredObjectVersionPersisted
|
export interface StoredObjectVersionPersisted extends StoredObjectVersionCreated {
|
||||||
extends StoredObjectVersionCreated {
|
version: number,
|
||||||
version: number;
|
id: number,
|
||||||
id: number;
|
createdAt: DateTime|null,
|
||||||
createdAt: DateTime | null;
|
createdBy: User|null,
|
||||||
createdBy: User | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoredObjectStatusChange {
|
export interface StoredObjectStatusChange {
|
||||||
id: number;
|
id: number,
|
||||||
filename: string;
|
filename: string,
|
||||||
status: StoredObjectStatus;
|
status: StoredObjectStatus,
|
||||||
type: string;
|
type: string,
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoredObjectVersionWithPointInTime extends StoredObjectVersionPersisted {
|
|
||||||
"point-in-times": StoredObjectPointInTime[];
|
|
||||||
"from-restored": StoredObjectVersionPersisted|null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoredObjectPointInTime {
|
|
||||||
id: number;
|
|
||||||
byUser: User | null;
|
|
||||||
reason: 'keep-before-conversion'|'keep-by-user';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function executed by the WopiEditButton component.
|
* Function executed by the WopiEditButton component.
|
||||||
*/
|
*/
|
||||||
export type WopiEditButtonExecutableBeforeLeaveFunction = {
|
export type WopiEditButtonExecutableBeforeLeaveFunction = {
|
||||||
(): Promise<void>;
|
(): Promise<void>
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object containing information for performering a POST request to a swift object store
|
* Object containing information for performering a POST request to a swift object store
|
||||||
*/
|
*/
|
||||||
export interface PostStoreObjectSignature {
|
export interface PostStoreObjectSignature {
|
||||||
method: "POST";
|
method: "POST",
|
||||||
max_file_size: number;
|
max_file_size: number,
|
||||||
max_file_count: 1;
|
max_file_count: 1,
|
||||||
expires: number;
|
expires: number,
|
||||||
submit_delay: 180;
|
submit_delay: 180,
|
||||||
redirect: string;
|
redirect: string,
|
||||||
prefix: string;
|
prefix: string,
|
||||||
url: string;
|
url: string,
|
||||||
signature: string;
|
signature: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PDFPage {
|
export interface PDFPage {
|
||||||
index: number;
|
index: number,
|
||||||
width: number;
|
width: number,
|
||||||
height: number;
|
height: number,
|
||||||
}
|
}
|
||||||
export interface SignatureZone {
|
export interface SignatureZone {
|
||||||
index: number | null;
|
index: number,
|
||||||
x: number;
|
x: number,
|
||||||
y: number;
|
y: number,
|
||||||
width: number;
|
width: number,
|
||||||
height: number;
|
height: number,
|
||||||
PDFPage: PDFPage;
|
PDFPage: PDFPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Signature {
|
export interface Signature {
|
||||||
id: number;
|
id: number,
|
||||||
storedObject: StoredObject;
|
storedObject: StoredObject,
|
||||||
zones: SignatureZone[];
|
zones: SignatureZone[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignedState =
|
export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error';
|
||||||
| "pending"
|
|
||||||
| "signed"
|
|
||||||
| "rejected"
|
|
||||||
| "canceled"
|
|
||||||
| "error";
|
|
||||||
|
|
||||||
export interface CheckSignature {
|
|
||||||
state: SignedState;
|
|
||||||
storedObject: StoredObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CanvasEvent = "select" | "add";
|
|
||||||
|
@@ -14,10 +14,7 @@
|
|||||||
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
|
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="isDownloadable">
|
<li v-if="isDownloadable">
|
||||||
<download-button :stored-object="props.storedObject" :at-version="props.storedObject.currentVersion" :filename="filename" :classes="{'dropdown-item': true}" :display-action-string-in-button="true"></download-button>
|
<download-button :stored-object="props.storedObject" :at-version="props.storedObject.currentVersion" :filename="filename" :classes="{'dropdown-item': true}"></download-button>
|
||||||
</li>
|
|
||||||
<li v-if="isHistoryViewable">
|
|
||||||
<history-button :stored-object="props.storedObject" :can-edit="canEdit && props.storedObject._permissions.canEdit"></history-button>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +40,6 @@ import {
|
|||||||
WopiEditButtonExecutableBeforeLeaveFunction
|
WopiEditButtonExecutableBeforeLeaveFunction
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
|
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
|
||||||
import HistoryButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton.vue";
|
|
||||||
|
|
||||||
interface DocumentActionButtonsGroupConfig {
|
interface DocumentActionButtonsGroupConfig {
|
||||||
storedObject: StoredObject,
|
storedObject: StoredObject,
|
||||||
@@ -130,11 +126,7 @@ const isConvertibleToPdf = computed<boolean>(() => {
|
|||||||
&& is_extension_viewable(props.storedObject.currentVersion.type)
|
&& is_extension_viewable(props.storedObject.currentVersion.type)
|
||||||
&& props.storedObject.currentVersion.type !== 'application/pdf'
|
&& props.storedObject.currentVersion.type !== 'application/pdf'
|
||||||
&& props.storedObject.currentVersion.persisted !== false;
|
&& props.storedObject.currentVersion.persisted !== false;
|
||||||
});
|
})
|
||||||
|
|
||||||
const isHistoryViewable = computed<boolean>(() => {
|
|
||||||
return props.storedObject.status === 'ready';
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkForReady = function(): void {
|
const checkForReady = function(): void {
|
||||||
if (
|
if (
|
||||||
|
@@ -26,120 +26,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</modal>
|
</modal>
|
||||||
</teleport>
|
</teleport>
|
||||||
<div class="col-12 m-auto">
|
<div class="col-12">
|
||||||
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
|
<div
|
||||||
<div v-if="pageCount > 1" class="col text-center turn-page">
|
class="row justify-content-center mb-2"
|
||||||
<button
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
:disabled="page <= 1"
|
|
||||||
@click="turnPage(-1)"
|
|
||||||
>
|
|
||||||
❮
|
|
||||||
</button>
|
|
||||||
<span>{{ page }}/{{ pageCount }}</span>
|
|
||||||
<button
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
:disabled="page >= pageCount"
|
|
||||||
@click="turnPage(1)"
|
|
||||||
>
|
|
||||||
❯
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="signature.zones.length > 1" class="col-3 p-0">
|
|
||||||
<button
|
|
||||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
@click="turnSignature(-1)"
|
|
||||||
>
|
|
||||||
{{ $t("last_zone") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="signature.zones.length > 1" class="col-3 p-0">
|
|
||||||
<button
|
|
||||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
@click="turnSignature(1)"
|
|
||||||
>
|
|
||||||
{{ $t("next_zone") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col text-end p-0">
|
|
||||||
<button
|
|
||||||
class="btn btn-misc btn-sm"
|
|
||||||
:hidden="!userSignatureZone"
|
|
||||||
@click="undoSign"
|
|
||||||
v-if="signature.zones.length > 1"
|
v-if="signature.zones.length > 1"
|
||||||
:title="$t('choose_another_signature')"
|
|
||||||
>
|
|
||||||
{{ $t("another_zone") }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-misc btn-sm"
|
|
||||||
:hidden="!userSignatureZone"
|
|
||||||
@click="undoSign"
|
|
||||||
v-else
|
|
||||||
>
|
|
||||||
{{ $t("cancel") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-1" v-if="signedState !== 'signed'">
|
|
||||||
<button
|
|
||||||
class="btn btn-create btn-sm"
|
|
||||||
:class="{ active: canvasEvent === 'add' }"
|
|
||||||
@click="toggleAddZone()"
|
|
||||||
:title="$t('add_sign_zone')"
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="row justify-content-center border-bottom pdf-tools d-none d-md-flex"
|
|
||||||
>
|
|
||||||
<div v-if="pageCount > 1" class="col-2 text-center turn-page p-0">
|
|
||||||
<button
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
:disabled="page <= 1"
|
|
||||||
@click="turnPage(-1)"
|
|
||||||
>
|
|
||||||
❮
|
|
||||||
</button>
|
|
||||||
<span>{{ page }} / {{ pageCount }}</span>
|
|
||||||
<button
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
:disabled="page >= pageCount"
|
|
||||||
@click="turnPage(1)"
|
|
||||||
>
|
|
||||||
❯
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
|
||||||
class="col text-end d-xl-none"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
@click="turnSignature(-1)"
|
|
||||||
>
|
|
||||||
{{ $t("last_zone") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
|
||||||
class="col text-start d-xl-none"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
@click="turnSignature(1)"
|
|
||||||
>
|
|
||||||
{{ $t("next_zone") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
|
||||||
class="col text-end d-none d-xl-flex p-0"
|
|
||||||
>
|
>
|
||||||
|
<div class="col-4 gap-2 d-grid">
|
||||||
<button
|
<button
|
||||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-sm"
|
||||||
@@ -148,10 +40,7 @@
|
|||||||
{{ $t("last_sign_zone") }}
|
{{ $t("last_sign_zone") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="col-4 gap-2 d-grid">
|
||||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
|
||||||
class="col text-start d-none d-xl-flex p-0"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-sm"
|
||||||
@@ -160,46 +49,39 @@
|
|||||||
{{ $t("next_sign_zone") }}
|
{{ $t("next_sign_zone") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-end p-0" v-if="signedState !== 'signed'">
|
|
||||||
<button
|
|
||||||
class="btn btn-misc btn-sm"
|
|
||||||
:hidden="!userSignatureZone"
|
|
||||||
@click="undoSign"
|
|
||||||
v-if="signature.zones.length > 1"
|
|
||||||
>
|
|
||||||
{{ $t("choose_another_signature") }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-misc btn-sm"
|
|
||||||
:hidden="!userSignatureZone"
|
|
||||||
@click="undoSign"
|
|
||||||
v-else
|
|
||||||
>
|
|
||||||
{{ $t("cancel") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col text-end p-0 pe-2 pe-xxl-4"
|
id="turn-page"
|
||||||
v-if="signedState !== 'signed'"
|
class="row justify-content-center mb-2"
|
||||||
|
v-if="pageCount > 1"
|
||||||
>
|
>
|
||||||
|
<div class="col-6-sm col-3-md text-center">
|
||||||
<button
|
<button
|
||||||
class="btn btn-create btn-sm"
|
class="btn btn-light btn-sm"
|
||||||
:class="{ active: canvasEvent === 'add' }"
|
:disabled="page <= 1"
|
||||||
@click="toggleAddZone()"
|
@click="turnPage(-1)"
|
||||||
:title="$t('add_sign_zone')"
|
|
||||||
>
|
>
|
||||||
{{ $t("add_zone") }}
|
❮
|
||||||
|
</button>
|
||||||
|
<span>page {{ page }} / {{ pageCount }}</span>
|
||||||
|
<button
|
||||||
|
class="btn btn-light btn-sm"
|
||||||
|
:disabled="page >= pageCount"
|
||||||
|
@click="turnPage(1)"
|
||||||
|
>
|
||||||
|
❯
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center">
|
<div class="col-12 text-center">
|
||||||
<canvas class="m-auto" id="canvas"></canvas>
|
<canvas class="m-auto" id="canvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xs-12 col-md-12 col-lg-9 m-auto p-4" id="action-buttons">
|
<div class="col-12 p-4" id="action-buttons" v-if="signedState !== 'signed'">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-4" v-if="signedState !== 'signed'">
|
<div class="col-6">
|
||||||
<button
|
<button
|
||||||
class="btn btn-action me-2"
|
class="btn btn-action me-2"
|
||||||
:disabled="!userSignatureZone"
|
:disabled="!userSignatureZone"
|
||||||
@@ -208,18 +90,26 @@
|
|||||||
{{ $t("sign") }}
|
{{ $t("sign") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4" v-else></div>
|
<div class="col-6 d-flex justify-content-end">
|
||||||
<div class="col-8 d-flex justify-content-end">
|
<button
|
||||||
<a
|
class="btn btn-misc me-2"
|
||||||
class="btn btn-delete"
|
:hidden="!userSignatureZone"
|
||||||
v-if="signedState !== 'signed'"
|
@click="undoSign"
|
||||||
:href="getReturnPath()"
|
v-if="signature.zones.length > 1"
|
||||||
>
|
>
|
||||||
|
{{ $t("choose_another_signature") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-misc me-2"
|
||||||
|
:hidden="!userSignatureZone"
|
||||||
|
@click="undoSign"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
{{ $t("cancel") }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-delete" @click="undoSign">
|
||||||
{{ $t("cancel_signing") }}
|
{{ $t("cancel_signing") }}
|
||||||
</a>
|
</button>
|
||||||
<a class="btn btn-misc" v-else :href="getReturnPath()">
|
|
||||||
{{ $t("return") }}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,13 +119,7 @@
|
|||||||
import { ref, Ref, reactive } from "vue";
|
import { ref, Ref, reactive } from "vue";
|
||||||
import { useToast } from "vue-toast-notification";
|
import { useToast } from "vue-toast-notification";
|
||||||
import "vue-toast-notification/dist/theme-sugar.css";
|
import "vue-toast-notification/dist/theme-sugar.css";
|
||||||
import {
|
import { Signature, SignatureZone, SignedState } from "../../types";
|
||||||
CanvasEvent,
|
|
||||||
CheckSignature,
|
|
||||||
Signature,
|
|
||||||
SignatureZone,
|
|
||||||
SignedState,
|
|
||||||
} from "../../types";
|
|
||||||
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
import * as pdfjsLib from "pdfjs-dist";
|
||||||
import {
|
import {
|
||||||
@@ -251,18 +135,19 @@ console.log(PdfWorker); // incredible but this is needed
|
|||||||
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
|
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
|
||||||
|
|
||||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||||
import { download_and_decrypt_doc } from "../StoredObjectButton/helpers";
|
import {
|
||||||
|
download_and_decrypt_doc,
|
||||||
|
} from "../StoredObjectButton/helpers";
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
|
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
|
||||||
|
|
||||||
const modalOpen: Ref<boolean> = ref(false);
|
const modalOpen: Ref<boolean> = ref(false);
|
||||||
const loading: Ref<boolean> = ref(false);
|
const loading: Ref<boolean> = ref(false);
|
||||||
const adding: Ref<boolean> = ref(false);
|
|
||||||
const canvasEvent: Ref<CanvasEvent> = ref("select");
|
|
||||||
const signedState: Ref<SignedState> = ref("pending");
|
const signedState: Ref<SignedState> = ref("pending");
|
||||||
const page: Ref<number> = ref(1);
|
const page: Ref<number> = ref(1);
|
||||||
const pageCount: Ref<number> = ref(0);
|
const pageCount: Ref<number> = ref(0);
|
||||||
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
|
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
|
||||||
|
let pdfSource: Ref<string> = ref("");
|
||||||
let pdf = {} as PDFDocumentProxy;
|
let pdf = {} as PDFDocumentProxy;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -275,13 +160,11 @@ const $toast = useToast();
|
|||||||
|
|
||||||
const signature = window.signature;
|
const signature = window.signature;
|
||||||
|
|
||||||
console.log(signature);
|
|
||||||
|
|
||||||
const mountPdf = async (url: string) => {
|
const mountPdf = async (url: string) => {
|
||||||
const loadingTask = pdfjsLib.getDocument(url);
|
const loadingTask = pdfjsLib.getDocument(url);
|
||||||
pdf = await loadingTask.promise;
|
pdf = await loadingTask.promise;
|
||||||
pageCount.value = pdf.numPages;
|
pageCount.value = pdf.numPages;
|
||||||
await setPage(page.value);
|
await setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
||||||
@@ -304,61 +187,59 @@ const setPage = async (page: number) => {
|
|||||||
await pdfPage.render(renderContext);
|
await pdfPage.render(renderContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
const init = () => downloadAndOpen().then(initPdf);
|
|
||||||
|
|
||||||
async function downloadAndOpen(): Promise<Blob> {
|
async function downloadAndOpen(): Promise<Blob> {
|
||||||
let raw;
|
let raw;
|
||||||
try {
|
try {
|
||||||
raw = await download_and_decrypt_doc(
|
raw = await download_and_decrypt_doc(signature.storedObject, signature.storedObject.currentVersion);
|
||||||
signature.storedObject,
|
|
||||||
signature.storedObject.currentVersion
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("error while downloading and decrypting document", e);
|
console.error("error while downloading and decrypting document", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
await mountPdf(URL.createObjectURL(raw));
|
await mountPdf(URL.createObjectURL(raw));
|
||||||
|
initPdf();
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initPdf = () => {
|
const initPdf = () => {
|
||||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||||
canvas.addEventListener("pointerup", canvasClick, false);
|
canvas.addEventListener(
|
||||||
setTimeout(() => drawAllZones(page.value), 800);
|
"pointerup",
|
||||||
|
(e: PointerEvent) => canvasClick(e, canvas),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
setTimeout(() => addZones(page.value), 800);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) =>
|
|
||||||
Math.round((x * canvasWidth) / PDFWidth);
|
|
||||||
|
|
||||||
const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) =>
|
|
||||||
Math.round((h * canvasHeight) / PDFHeight);
|
|
||||||
|
|
||||||
const hitSignature = (
|
const hitSignature = (
|
||||||
zone: SignatureZone,
|
zone: SignatureZone,
|
||||||
xy: number[],
|
xy: number[],
|
||||||
canvasWidth: number,
|
canvasWidth: number,
|
||||||
canvasHeight: number
|
canvasHeight: number
|
||||||
) =>
|
) => {
|
||||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
|
const scaleXToCanvas = (x: number) =>
|
||||||
xy[0] <
|
Math.round((x * canvasWidth) / zone.PDFPage.width);
|
||||||
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
|
const scaleHeightToCanvas = (h: number) =>
|
||||||
zone.PDFPage.height -
|
Math.round((h * canvasHeight) / zone.PDFPage.height);
|
||||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
|
const scaleYToCanvas = (y: number) =>
|
||||||
xy[1] &&
|
Math.round(zone.PDFPage.height - scaleHeightToCanvas(y));
|
||||||
xy[1] <
|
return (
|
||||||
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
|
scaleXToCanvas(zone.x) < xy[0] &&
|
||||||
zone.PDFPage.height;
|
xy[0] < scaleXToCanvas(zone.x + zone.width) &&
|
||||||
|
scaleYToCanvas(zone.y) < xy[1] &&
|
||||||
|
xy[1] < scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
||||||
userSignatureZone.value = z;
|
userSignatureZone.value = z;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
setPage(page.value);
|
setPage(page.value);
|
||||||
setTimeout(() => drawAllZones(page.value), 200);
|
setTimeout(() => drawZone(z, ctx, canvas.width, canvas.height), 200);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
const canvasClick = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||||||
signature.zones
|
signature.zones
|
||||||
.filter((z) => z.PDFPage.index + 1 === page.value)
|
.filter((z) => z.PDFPage.index + 1 === page.value)
|
||||||
.map((z) => {
|
.map((z) => {
|
||||||
@@ -375,18 +256,11 @@ const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const canvasClick = (e: PointerEvent) => {
|
|
||||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
|
||||||
canvasEvent.value === "select"
|
|
||||||
? selectZoneEvent(e, canvas)
|
|
||||||
: addZoneEvent(e, canvas);
|
|
||||||
};
|
|
||||||
|
|
||||||
const turnPage = async (upOrDown: number) => {
|
const turnPage = async (upOrDown: number) => {
|
||||||
//userSignatureZone.value = null; // desactivate the reset of the zone when turning page
|
userSignatureZone.value = null;
|
||||||
page.value = page.value + upOrDown;
|
page.value = page.value + upOrDown;
|
||||||
await setPage(page.value);
|
await setPage(page.value);
|
||||||
setTimeout(() => drawAllZones(page.value), 200);
|
setTimeout(() => addZones(page.value), 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
const turnSignature = async (upOrDown: number) => {
|
const turnSignature = async (upOrDown: number) => {
|
||||||
@@ -416,6 +290,12 @@ const drawZone = (
|
|||||||
) => {
|
) => {
|
||||||
const unselectedBlue = "#007bff";
|
const unselectedBlue = "#007bff";
|
||||||
const selectedBlue = "#034286";
|
const selectedBlue = "#034286";
|
||||||
|
const scaleXToCanvas = (x: number) =>
|
||||||
|
Math.round((x * canvasWidth) / zone.PDFPage.width);
|
||||||
|
const scaleHeightToCanvas = (h: number) =>
|
||||||
|
Math.round((h * canvasHeight) / zone.PDFPage.height);
|
||||||
|
const scaleYToCanvas = (y: number) =>
|
||||||
|
Math.round(zone.PDFPage.height - scaleHeightToCanvas(y));
|
||||||
ctx.strokeStyle =
|
ctx.strokeStyle =
|
||||||
userSignatureZone.value?.index === zone.index
|
userSignatureZone.value?.index === zone.index
|
||||||
? selectedBlue
|
? selectedBlue
|
||||||
@@ -423,22 +303,16 @@ const drawZone = (
|
|||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.lineJoin = "bevel";
|
ctx.lineJoin = "bevel";
|
||||||
ctx.strokeRect(
|
ctx.strokeRect(
|
||||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
|
scaleXToCanvas(zone.x),
|
||||||
zone.PDFPage.height -
|
scaleYToCanvas(zone.y),
|
||||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
|
scaleXToCanvas(zone.width),
|
||||||
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
|
scaleHeightToCanvas(zone.height)
|
||||||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
|
|
||||||
);
|
);
|
||||||
ctx.font = "bold 16px serif";
|
ctx.font = "bold 16px serif";
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.fillStyle = "black";
|
ctx.fillStyle = "black";
|
||||||
const xText =
|
const xText = scaleXToCanvas(zone.x) + scaleXToCanvas(zone.width) / 2;
|
||||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) +
|
const yText = scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height) / 2;
|
||||||
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2;
|
|
||||||
const yText =
|
|
||||||
zone.PDFPage.height -
|
|
||||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
|
|
||||||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
|
|
||||||
if (userSignatureZone.value?.index === zone.index) {
|
if (userSignatureZone.value?.index === zone.index) {
|
||||||
ctx.fillStyle = selectedBlue;
|
ctx.fillStyle = selectedBlue;
|
||||||
ctx.fillText("Signer ici", xText, yText);
|
ctx.fillText("Signer ici", xText, yText);
|
||||||
@@ -446,33 +320,27 @@ const drawZone = (
|
|||||||
ctx.fillStyle = unselectedBlue;
|
ctx.fillStyle = unselectedBlue;
|
||||||
ctx.fillText("Choisir cette", xText, yText - 12);
|
ctx.fillText("Choisir cette", xText, yText - 12);
|
||||||
ctx.fillText("zone de signature", xText, yText + 12);
|
ctx.fillText("zone de signature", xText, yText + 12);
|
||||||
|
// ctx.strokeStyle = "#c6c6c6"; // halo
|
||||||
|
// ctx.strokeText("Choisir cette", xText, yText - 12);
|
||||||
|
// ctx.strokeText("zone de signature", xText, yText + 12);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawAllZones = (page: number) => {
|
const addZones = (page: number) => {
|
||||||
const canvas = document.querySelectorAll("canvas")[0];
|
const canvas = document.querySelectorAll("canvas")[0];
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (ctx && signedState.value !== "signed") {
|
if (ctx) {
|
||||||
signature.zones
|
signature.zones
|
||||||
.filter((z) => z.PDFPage.index + 1 === page)
|
.filter((z) => z.PDFPage.index + 1 === page)
|
||||||
.map((z) => {
|
.map((z) => drawZone(z, ctx, canvas.width, canvas.height));
|
||||||
if (userSignatureZone.value) {
|
|
||||||
if (userSignatureZone.value?.index === z.index) {
|
|
||||||
drawZone(z, ctx, canvas.width, canvas.height);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
drawZone(z, ctx, canvas.width, canvas.height);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkSignature = () => {
|
const checkSignature = () => {
|
||||||
const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
|
const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
|
||||||
return makeFetch<null, CheckSignature>("GET", url)
|
return makeFetch("GET", url)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
signedState.value = r.state;
|
signedState.value = r as SignedState;
|
||||||
signature.storedObject = r.storedObject;
|
|
||||||
checkForReady();
|
checkForReady();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -546,66 +414,22 @@ const confirmSign = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const undoSign = async () => {
|
const undoSign = async () => {
|
||||||
signature.zones = signature.zones.filter((z) => z.index !== null);
|
// const canvas = document.querySelectorAll("canvas")[0];
|
||||||
|
// const ctx = canvas.getContext("2d");
|
||||||
|
// if (ctx && userSignatureZone.value) {
|
||||||
|
// //drawZone(userSignatureZone.value, ctx, canvas.width, canvas.height);
|
||||||
|
// }
|
||||||
await setPage(page.value);
|
await setPage(page.value);
|
||||||
setTimeout(() => drawAllZones(page.value), 200);
|
setTimeout(() => addZones(page.value), 200);
|
||||||
userSignatureZone.value = null;
|
userSignatureZone.value = null;
|
||||||
adding.value = false;
|
|
||||||
canvasEvent.value = "select";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAddZone = () => {
|
downloadAndOpen();
|
||||||
canvasEvent.value === "select"
|
|
||||||
? (canvasEvent.value = "add")
|
|
||||||
: (canvasEvent.value = "select");
|
|
||||||
};
|
|
||||||
|
|
||||||
const addZoneEvent = async (e: PointerEvent, canvas: HTMLCanvasElement) => {
|
|
||||||
const BOX_WIDTH = 180;
|
|
||||||
const BOX_HEIGHT = 90;
|
|
||||||
const PDFPageHeight = canvas.height;
|
|
||||||
const PDFPageWidth = canvas.width;
|
|
||||||
|
|
||||||
const x = e.offsetX;
|
|
||||||
const y = e.offsetY;
|
|
||||||
const newZone: SignatureZone = {
|
|
||||||
index: null,
|
|
||||||
x:
|
|
||||||
scaleXToCanvas(x, canvas.width, PDFPageWidth) -
|
|
||||||
scaleXToCanvas(BOX_WIDTH / 2, canvas.width, PDFPageWidth),
|
|
||||||
y:
|
|
||||||
PDFPageHeight -
|
|
||||||
scaleYToCanvas(y, canvas.height, PDFPageHeight) +
|
|
||||||
scaleYToCanvas(BOX_HEIGHT / 2, canvas.height, PDFPageHeight),
|
|
||||||
width: BOX_WIDTH,
|
|
||||||
height: BOX_HEIGHT,
|
|
||||||
PDFPage: {
|
|
||||||
index: page.value - 1,
|
|
||||||
width: PDFPageWidth,
|
|
||||||
height: PDFPageHeight,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
signature.zones.push(newZone);
|
|
||||||
userSignatureZone.value = newZone;
|
|
||||||
|
|
||||||
await setPage(page.value);
|
|
||||||
setTimeout(() => drawAllZones(page.value), 200);
|
|
||||||
canvasEvent.value = "select";
|
|
||||||
adding.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getReturnPath = () =>
|
|
||||||
window.location.search
|
|
||||||
? window.location.search.split("?returnPath=")[1] ??
|
|
||||||
window.location.pathname
|
|
||||||
: window.location.pathname;
|
|
||||||
|
|
||||||
init();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
#canvas {
|
#canvas {
|
||||||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
div#action-buttons {
|
div#action-buttons {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@@ -613,15 +437,7 @@ div#action-buttons {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
div.pdf-tools {
|
div#turn-page {
|
||||||
background-color: #f3f3f3;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
@media (min-width: 1400px) {
|
|
||||||
// background: none;
|
|
||||||
// border: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div.turn-page {
|
|
||||||
span {
|
span {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
margin: 0 0.4rem;
|
margin: 0 0.4rem;
|
||||||
|
@@ -10,20 +10,13 @@ const appMessages = {
|
|||||||
you_are_going_to_sign: 'Vous allez signer le document',
|
you_are_going_to_sign: 'Vous allez signer le document',
|
||||||
signature_confirmation: 'Confirmation de la signature',
|
signature_confirmation: 'Confirmation de la signature',
|
||||||
sign: 'Signer',
|
sign: 'Signer',
|
||||||
choose_another_signature: 'Choisir une autre zone',
|
choose_another_signature: 'Choisir une autre zone de signature',
|
||||||
cancel: 'Annuler',
|
cancel: 'Annuler',
|
||||||
cancel_signing: 'Refuser de signer',
|
cancel_signing: 'Refuser de signer',
|
||||||
last_sign_zone: 'Zone de signature précédente',
|
last_sign_zone: 'Zone de signature précédente',
|
||||||
next_sign_zone: 'Zone de signature suivante',
|
next_sign_zone: 'Zone de signature suivante',
|
||||||
add_sign_zone: 'Ajouter une zone de signature',
|
|
||||||
last_zone: 'Zone précédente',
|
|
||||||
next_zone: 'Zone suivante',
|
|
||||||
add_zone: 'Ajouter une zone',
|
|
||||||
another_zone: 'Autre zone',
|
|
||||||
electronic_signature_in_progress: 'Signature électronique en cours...',
|
electronic_signature_in_progress: 'Signature électronique en cours...',
|
||||||
loading: 'Chargement...',
|
loading: 'Chargement...'
|
||||||
remove_sign_zone: 'Enlever la zone',
|
|
||||||
return: 'Retour',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
import {StoredObject, StoredObjectVersionCreated} from "../../types";
|
import {StoredObject, StoredObjectVersionCreated} from "../../types";
|
||||||
import {encryptFile, fetchNewStoredObject, uploadVersion} from "../../js/async-upload/uploader";
|
import {encryptFile, fetchNewStoredObject, uploadVersion} from "../../js/async-upload/uploader";
|
||||||
import {computed, ref, Ref} from "vue";
|
import {computed, ref, Ref} from "vue";
|
||||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
|
||||||
|
|
||||||
interface DropFileConfig {
|
interface DropFileConfig {
|
||||||
existingDoc?: StoredObject,
|
existingDoc?: StoredObject,
|
||||||
@@ -17,7 +16,6 @@ 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;
|
||||||
@@ -79,7 +77,6 @@ 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
|
||||||
@@ -111,11 +108,18 @@ 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" class="file-icon">
|
<p v-if="has_existing_doc">
|
||||||
<file-icon :type="props.existingDoc?.type"></file-icon>
|
<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.openxmlformats-officedocument.wordprocessingml.document'"></i>
|
||||||
|
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/msword'"></i>
|
||||||
|
<i class="fa fa-file-excel-o" v-else-if="props.existingDoc?.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'"></i>
|
||||||
|
<i class="fa fa-file-excel-o" v-else-if="props.existingDoc?.type === 'application/vnd.ms-excel'"></i>
|
||||||
|
<i class="fa fa-file-image-o" v-else-if="props.existingDoc?.type === 'image/jpeg'"></i>
|
||||||
|
<i class="fa fa-file-image-o" v-else-if="props.existingDoc?.type === 'image/png'"></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>
|
||||||
</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>
|
||||||
@@ -131,18 +135,9 @@ 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: 10rem;
|
height: 8rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -163,5 +158,4 @@ const handleFile = async (file: File): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,25 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
interface FileIconConfig {
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<FileIconConfig>();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<i class="fa fa-file-pdf-o" v-if="props.type === 'application/pdf'"></i>
|
|
||||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/vnd.oasis.opendocument.text'"></i>
|
|
||||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'"></i>
|
|
||||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/msword'"></i>
|
|
||||||
<i class="fa fa-file-excel-o" v-else-if="props.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'"></i>
|
|
||||||
<i class="fa fa-file-excel-o" v-else-if="props.type === 'application/vnd.ms-excel'"></i>
|
|
||||||
<i class="fa fa-file-image-o" v-else-if="props.type === 'image/jpeg'"></i>
|
|
||||||
<i class="fa fa-file-image-o" v-else-if="props.type === 'image/png'"></i>
|
|
||||||
<i class="fa fa-file-archive-o" v-else-if="props.type === 'application/x-zip-compressed'"></i>
|
|
||||||
<i class="fa fa-file-code-o" v-else ></i>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<a :class="props.classes" @click="download_and_open($event)" ref="btn">
|
<a :class="props.classes" @click="download_and_open($event)">
|
||||||
<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, ref} from "vue";
|
import {reactive} from "vue";
|
||||||
import {StoredObject} from "../../types";
|
import {StoredObject} from "../../types";
|
||||||
|
|
||||||
interface ConvertButtonConfig {
|
interface ConvertButtonConfig {
|
||||||
@@ -24,7 +24,6 @@ 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;
|
||||||
@@ -42,14 +41,6 @@ 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>
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open($event)" title="Télécharger">
|
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open($event)">
|
||||||
<i class="fa fa-download"></i>
|
<i class="fa fa-download"></i>
|
||||||
<template v-if="displayActionStringInButton">Télécharger</template>
|
Télécharger
|
||||||
</a>
|
</a>
|
||||||
<a v-else :class="props.classes" target="_blank" :type="props.storedObject.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button" title="Ouvrir">
|
<a v-else :class="props.classes" target="_blank" :type="props.storedObject.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button">
|
||||||
<i class="fa fa-external-link"></i>
|
<i class="fa fa-external-link"></i>
|
||||||
<template v-if="displayActionStringInButton">Ouvrir</template>
|
Ouvrir
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ interface DownloadButtonConfig {
|
|||||||
atVersion: StoredObjectVersion,
|
atVersion: StoredObjectVersion,
|
||||||
classes: { [k: string]: boolean },
|
classes: { [k: string]: boolean },
|
||||||
filename?: string,
|
filename?: string,
|
||||||
displayActionStringInButton: boolean,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DownloadButtonState {
|
interface DownloadButtonState {
|
||||||
@@ -29,7 +28,7 @@ interface DownloadButtonState {
|
|||||||
href_url: string,
|
href_url: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<DownloadButtonConfig>(), {displayActionStringInButton: true});
|
const props = defineProps<DownloadButtonConfig>();
|
||||||
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
|
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
|
||||||
|
|
||||||
const open_button = ref<HTMLAnchorElement | null>(null);
|
const open_button = ref<HTMLAnchorElement | null>(null);
|
||||||
@@ -77,15 +76,6 @@ 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>
|
||||||
|
|
||||||
|
@@ -1,57 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import HistoryButtonModal from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonModal.vue";
|
|
||||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../types";
|
|
||||||
import {computed, reactive, ref, useTemplateRef} from "vue";
|
|
||||||
import {get_versions} from "./HistoryButton/api";
|
|
||||||
|
|
||||||
interface HistoryButtonConfig {
|
|
||||||
storedObject: StoredObject;
|
|
||||||
canEdit: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HistoryButtonState {
|
|
||||||
versions: StoredObjectVersionWithPointInTime[];
|
|
||||||
loaded: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<HistoryButtonConfig>();
|
|
||||||
const state = reactive<HistoryButtonState>({versions: [], loaded: false});
|
|
||||||
const modal = useTemplateRef<typeof HistoryButtonModal>('modal');
|
|
||||||
|
|
||||||
const download_version_and_open_modal = async function (): Promise<void> {
|
|
||||||
if (null !== modal.value) {
|
|
||||||
modal.value.open();
|
|
||||||
} else {
|
|
||||||
console.log("modal is null");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.loaded) {
|
|
||||||
const versions = await get_versions(props.storedObject);
|
|
||||||
|
|
||||||
for (const version of versions) {
|
|
||||||
state.versions.push(version);
|
|
||||||
}
|
|
||||||
state.loaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRestoreVersion = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
|
||||||
state.versions.unshift(newVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<a @click="download_version_and_open_modal" class="dropdown-item">
|
|
||||||
<history-button-modal ref="modal" :versions="state.versions" :stored-object="storedObject" :can-edit="canEdit" @restore-version="onRestoreVersion"></history-button-modal>
|
|
||||||
<i class="fa fa-history"></i>
|
|
||||||
Historique
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
i.fa::before {
|
|
||||||
color: var(--bs-dropdown-link-hover-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,68 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../../types";
|
|
||||||
import HistoryButtonListItem from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonListItem.vue";
|
|
||||||
import {computed, reactive} from "vue";
|
|
||||||
|
|
||||||
interface HistoryButtonListConfig {
|
|
||||||
versions: StoredObjectVersionWithPointInTime[];
|
|
||||||
storedObject: StoredObject;
|
|
||||||
canEdit: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
interface HistoryButtonListState {
|
|
||||||
/**
|
|
||||||
* Contains the number of the newly created version when a version is restored.
|
|
||||||
*/
|
|
||||||
restored: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<HistoryButtonListConfig>();
|
|
||||||
|
|
||||||
const state = reactive<HistoryButtonListState>({restored: -1})
|
|
||||||
|
|
||||||
const higher_version = computed<number>(() => props.versions.reduce(
|
|
||||||
(accumulator: number, version: StoredObjectVersionWithPointInTime) => Math.max(accumulator, version.version),
|
|
||||||
-1
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executed when a version in child component is restored.
|
|
||||||
*
|
|
||||||
* internally, keep track of the newly restored version
|
|
||||||
*/
|
|
||||||
const onRestored = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
|
||||||
state.restored = newVersion.version;
|
|
||||||
emit('restoreVersion', {newVersion});
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<template v-if="props.versions.length > 0">
|
|
||||||
<div class="container">
|
|
||||||
<template v-for="v in props.versions">
|
|
||||||
<history-button-list-item
|
|
||||||
:version="v"
|
|
||||||
:can-edit="canEdit"
|
|
||||||
:is-current="higher_version === v.version"
|
|
||||||
:is-restored="v.version === state.restored"
|
|
||||||
:stored-object="storedObject"
|
|
||||||
@restore-version="onRestored"
|
|
||||||
></history-button-list-item>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<p>Chargement des versions</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,113 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {StoredObject, StoredObjectPointInTime, StoredObjectVersionWithPointInTime} from "./../../../types";
|
|
||||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
|
||||||
import {ISOToDatetime} from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
|
||||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
|
||||||
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
|
|
||||||
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
|
||||||
import {computed} from "vue";
|
|
||||||
|
|
||||||
interface HistoryButtonListItemConfig {
|
|
||||||
version: StoredObjectVersionWithPointInTime;
|
|
||||||
storedObject: StoredObject;
|
|
||||||
canEdit: boolean;
|
|
||||||
isCurrent: boolean;
|
|
||||||
isRestored: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const props = defineProps<HistoryButtonListItemConfig>();
|
|
||||||
|
|
||||||
const onRestore = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
|
||||||
emit('restoreVersion', {newVersion});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isKeptBeforeConversion = computed<boolean>(() => props.version["point-in-times"].reduce(
|
|
||||||
(accumulator: boolean, pit: StoredObjectPointInTime) => accumulator || "keep-before-conversion" === pit.reason,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isRestored = computed<boolean>(() => null !== props.version["from-restored"]);
|
|
||||||
|
|
||||||
const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, 'blinking-2': boolean}>(() => ({row: true, 'row-hover': true, 'blinking-1': props.isRestored && 0 === props.version.version % 2, 'blinking-2': props.isRestored && 1 === props.version.version % 2}));
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="classes">
|
|
||||||
<div class="col-12 tags" v-if="isCurrent || isKeptBeforeConversion || isRestored">
|
|
||||||
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
|
|
||||||
<span class="badge bg-info" v-if="isKeptBeforeConversion">Conservée avant conversion dans un autre format</span>
|
|
||||||
<span class="badge bg-info" v-if="isRestored">Restaurée depuis la version {{ version["from-restored"]?.version }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<file-icon :type="version.type"></file-icon> <span><strong>#{{ version.version + 1 }}</strong></span> <strong v-if="version.version == 0">Créé par</strong><strong v-else>modifié par</strong> <span class="badge-user"><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge></span> <strong>à</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<ul class="record_actions small slim on-version-actions">
|
|
||||||
<li v-if="canEdit && !isCurrent">
|
|
||||||
<restore-version-button :stored-object-version="props.version" @restore-version="onRestore"></restore-version-button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<download-button :stored-object="storedObject" :at-version="version" :classes="{btn: true, 'btn-outline-primary': true}" :display-action-string-in-button="false"></download-button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
div.tags {
|
|
||||||
span.badge:not(:last-child) {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// to make the animation restart, we have the same animation twice,
|
|
||||||
// and alternate between both
|
|
||||||
.blinking-1 {
|
|
||||||
animation-name: backgroundColorPalette-1;
|
|
||||||
animation-duration: 8s;
|
|
||||||
animation-iteration-count: 1;
|
|
||||||
animation-direction: normal;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
}
|
|
||||||
@keyframes backgroundColorPalette-1 {
|
|
||||||
0% {
|
|
||||||
background: var(--bs-chill-green-dark);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
background: var(--bs-chill-green);
|
|
||||||
}
|
|
||||||
65% {
|
|
||||||
background: var(--bs-chill-beige);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.blinking-2 {
|
|
||||||
animation-name: backgroundColorPalette-2;
|
|
||||||
animation-duration: 8s;
|
|
||||||
animation-iteration-count: 1;
|
|
||||||
animation-direction: normal;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
}
|
|
||||||
@keyframes backgroundColorPalette-2 {
|
|
||||||
0% {
|
|
||||||
background: var(--bs-chill-green-dark);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
background: var(--bs-chill-green);
|
|
||||||
}
|
|
||||||
65% {
|
|
||||||
background: var(--bs-chill-beige);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,48 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
|
||||||
import {reactive} from "vue";
|
|
||||||
import HistoryButtonList from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonList.vue";
|
|
||||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../../types";
|
|
||||||
|
|
||||||
interface HistoryButtonListConfig {
|
|
||||||
versions: StoredObjectVersionWithPointInTime[];
|
|
||||||
storedObject: StoredObject;
|
|
||||||
canEdit: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
interface HistoryButtonModalState {
|
|
||||||
opened: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<HistoryButtonListConfig>();
|
|
||||||
const state = reactive<HistoryButtonModalState>({opened: false});
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
console.log('open');
|
|
||||||
state.opened = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({open});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<Teleport to="body">
|
|
||||||
<modal v-if="state.opened" @close="state.opened = false">
|
|
||||||
<template v-slot:header>
|
|
||||||
<h3>Historique des versions du document</h3>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body>
|
|
||||||
<p>Les versions sont conservées pendant 90 jours.</p>
|
|
||||||
<history-button-list :versions="props.versions" :can-edit="canEdit" :stored-object="storedObject" @restore-version="(payload) => emit('restoreVersion', payload)"></history-button-list>
|
|
||||||
</template>
|
|
||||||
</modal>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,32 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {StoredObjectVersionPersisted, StoredObjectVersionWithPointInTime} from "../../../types";
|
|
||||||
import {useToast} from "vue-toast-notification";
|
|
||||||
import {restore_version} from "./api";
|
|
||||||
|
|
||||||
interface RestoreVersionButtonProps {
|
|
||||||
storedObjectVersion: StoredObjectVersionPersisted,
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const props = defineProps<RestoreVersionButtonProps>()
|
|
||||||
|
|
||||||
const $toast = useToast();
|
|
||||||
|
|
||||||
const restore_version_fn = async () => {
|
|
||||||
const newVersion = await restore_version(props.storedObjectVersion);
|
|
||||||
|
|
||||||
$toast.success("Version restaurée");
|
|
||||||
emit('restoreVersion', {newVersion});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<button class="btn btn-outline-action" @click="restore_version_fn" title="Restaurer"><i class="fa fa-rotate-left"></i> Restaurer</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,12 +0,0 @@
|
|||||||
import {StoredObject, StoredObjectVersionPersisted, StoredObjectVersionWithPointInTime} from "../../../types";
|
|
||||||
import {fetchResults, makeFetch} from "../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
|
||||||
|
|
||||||
export const get_versions = async (storedObject: StoredObject): Promise<StoredObjectVersionWithPointInTime[]> => {
|
|
||||||
const versions = await fetchResults<StoredObjectVersionWithPointInTime>(`/api/1.0/doc-store/stored-object/${storedObject.uuid}/versions`);
|
|
||||||
|
|
||||||
return versions.sort((a: StoredObjectVersionWithPointInTime, b: StoredObjectVersionWithPointInTime) => b.version - a.version);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const restore_version = async (version: StoredObjectVersionPersisted): Promise<StoredObjectVersionWithPointInTime> => {
|
|
||||||
return await makeFetch<null, StoredObjectVersionWithPointInTime>("POST", `/api/1.0/doc-store/stored-object/restore-from-version/${version.id}`);
|
|
||||||
}
|
|
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Serializer\Normalizer;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
|
||||||
|
|
||||||
class StoredObjectPointInTimeNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
|
||||||
{
|
|
||||||
use NormalizerAwareTrait;
|
|
||||||
|
|
||||||
public function normalize($object, ?string $format = null, array $context = [])
|
|
||||||
{
|
|
||||||
/* @var StoredObjectPointInTime $object */
|
|
||||||
return [
|
|
||||||
'id' => $object->getId(),
|
|
||||||
'reason' => $object->getReason()->value,
|
|
||||||
'byUser' => $this->normalizer->normalize($object->getByUser(), $format, [AbstractNormalizer::GROUPS => 'read']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function supportsNormalization($data, ?string $format = null)
|
|
||||||
{
|
|
||||||
return $data instanceof StoredObjectPointInTime;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -12,8 +12,6 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Serializer\Normalizer;
|
namespace Chill\DocStoreBundle\Serializer\Normalizer;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||||
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
@@ -22,17 +20,13 @@ class StoredObjectVersionNormalizer implements NormalizerInterface, NormalizerAw
|
|||||||
{
|
{
|
||||||
use NormalizerAwareTrait;
|
use NormalizerAwareTrait;
|
||||||
|
|
||||||
final public const WITH_POINT_IN_TIMES_CONTEXT = 'with-point-in-times';
|
|
||||||
|
|
||||||
final public const WITH_RESTORED_CONTEXT = 'with-restored';
|
|
||||||
|
|
||||||
public function normalize($object, ?string $format = null, array $context = [])
|
public function normalize($object, ?string $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
if (!$object instanceof StoredObjectVersion) {
|
if (!$object instanceof StoredObjectVersion) {
|
||||||
throw new \InvalidArgumentException('The object must be an instance of '.StoredObjectVersion::class);
|
throw new \InvalidArgumentException('The object must be an instance of '.StoredObjectVersion::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = [
|
return [
|
||||||
'id' => $object->getId(),
|
'id' => $object->getId(),
|
||||||
'filename' => $object->getFilename(),
|
'filename' => $object->getFilename(),
|
||||||
'version' => $object->getVersion(),
|
'version' => $object->getVersion(),
|
||||||
@@ -40,18 +34,8 @@ class StoredObjectVersionNormalizer implements NormalizerInterface, NormalizerAw
|
|||||||
'keyInfos' => $object->getKeyInfos(),
|
'keyInfos' => $object->getKeyInfos(),
|
||||||
'type' => $object->getType(),
|
'type' => $object->getType(),
|
||||||
'createdAt' => $this->normalizer->normalize($object->getCreatedAt(), $format, $context),
|
'createdAt' => $this->normalizer->normalize($object->getCreatedAt(), $format, $context),
|
||||||
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, [...$context, UserNormalizer::AT_DATE => $object->getCreatedAt()]),
|
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, $context),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
|
|
||||||
$data['point-in-times'] = $this->normalizer->normalize($object->getPointInTimes(), $format, $context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array(self::WITH_RESTORED_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
|
|
||||||
$data['from-restored'] = $this->normalizer->normalize($object->getCreatedFrom(), $format, [AbstractNormalizer::GROUPS => ['read']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supportsNormalization($data, ?string $format = null, array $context = [])
|
public function supportsNormalization($data, ?string $format = null, array $context = [])
|
||||||
|
@@ -18,7 +18,7 @@ final readonly class PdfSignedMessage
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly int $signatureId,
|
public readonly int $signatureId,
|
||||||
public readonly ?int $signatureZoneIndex,
|
public readonly int $signatureZoneIndex,
|
||||||
public readonly string $content,
|
public readonly string $content,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
@@ -12,11 +12,12 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
|
|
||||||
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
||||||
@@ -32,7 +33,7 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
|||||||
private StoredObjectManagerInterface $storedObjectManager,
|
private StoredObjectManagerInterface $storedObjectManager,
|
||||||
private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
|
private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
|
||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private SignatureStepStateChanger $signatureStepStateChanger,
|
private ClockInterface $clock,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function __invoke(PdfSignedMessage $message): void
|
public function __invoke(PdfSignedMessage $message): void
|
||||||
@@ -53,8 +54,8 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
|||||||
|
|
||||||
$this->storedObjectManager->write($storedObject, $message->content);
|
$this->storedObjectManager->write($storedObject, $message->content);
|
||||||
|
|
||||||
$this->signatureStepStateChanger->markSignatureAsSigned($signature, $message->signatureZoneIndex);
|
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate($this->clock->now());
|
||||||
|
$signature->setZoneSignatureIndex($message->signatureZoneIndex);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
$this->entityManager->clear();
|
$this->entityManager->clear();
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@ final readonly class RequestPdfSignMessage
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public int $signatureId,
|
public int $signatureId,
|
||||||
public PDFSignatureZone $PDFSignatureZone,
|
public PDFSignatureZone $PDFSignatureZone,
|
||||||
public ?int $signatureZoneIndex,
|
public int $signatureZoneIndex,
|
||||||
public string $reason,
|
public string $reason,
|
||||||
public string $signerText,
|
public string $signerText,
|
||||||
public string $content,
|
public string $content,
|
||||||
|
@@ -17,7 +17,7 @@ final readonly class PDFSignatureZone
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[Groups(['read'])]
|
#[Groups(['read'])]
|
||||||
public ?int $index,
|
public int $index,
|
||||||
#[Groups(['read'])]
|
#[Groups(['read'])]
|
||||||
public float $x,
|
public float $x,
|
||||||
#[Groups(['read'])]
|
#[Groups(['read'])]
|
||||||
|
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Service;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
|
|
||||||
final readonly class StoredObjectRestore implements StoredObjectRestoreInterface
|
|
||||||
{
|
|
||||||
public function __construct(private readonly StoredObjectManagerInterface $storedObjectManager, private readonly LoggerInterface $logger) {}
|
|
||||||
|
|
||||||
public function restore(StoredObjectVersion $storedObjectVersion): StoredObjectVersion
|
|
||||||
{
|
|
||||||
$oldContent = $this->storedObjectManager->read($storedObjectVersion);
|
|
||||||
|
|
||||||
$newVersion = $this->storedObjectManager->write($storedObjectVersion->getStoredObject(), $oldContent, $storedObjectVersion->getType());
|
|
||||||
|
|
||||||
$newVersion->setCreatedFrom($storedObjectVersion);
|
|
||||||
|
|
||||||
$this->logger->info('[StoredObjectRestore] Restore stored object version', [
|
|
||||||
'stored_object_uuid' => $storedObjectVersion->getStoredObject()->getUuid(),
|
|
||||||
'old_version_id' => $storedObjectVersion->getId(),
|
|
||||||
'old_version_version' => $storedObjectVersion->getVersion(),
|
|
||||||
'new_version_id' => $newVersion->getVersion(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $newVersion;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Service;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore an old version of the stored object as the current one.
|
|
||||||
*/
|
|
||||||
interface StoredObjectRestoreInterface
|
|
||||||
{
|
|
||||||
public function restore(StoredObjectVersion $storedObjectVersion): StoredObjectVersion;
|
|
||||||
}
|
|
@@ -1,74 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Tests\Chill\DocStoreBundle\Tests\Controller;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectRestoreInterface;
|
|
||||||
use Chill\DocStoreBundle\Controller\StoredObjectRestoreVersionApiController;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class StoredObjectRestoreVersionApiControllerTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testRestoreStoredObjectVersion(): void
|
|
||||||
{
|
|
||||||
$security = $this->createMock(Security::class);
|
|
||||||
$storedObjectRestore = $this->createMock(StoredObjectRestoreInterface::class);
|
|
||||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
|
||||||
$serializer = $this->createMock(SerializerInterface::class);
|
|
||||||
$storedObjectVersion = $this->createMock(StoredObjectVersion::class);
|
|
||||||
$controller = new StoredObjectRestoreVersionApiController($security, $storedObjectRestore, $entityManager, $serializer);
|
|
||||||
|
|
||||||
$security->expects($this->once())
|
|
||||||
->method('isGranted')
|
|
||||||
->willReturn(true);
|
|
||||||
$storedObjectRestore->expects($this->once())
|
|
||||||
->method('restore')
|
|
||||||
->willReturn($storedObjectVersion);
|
|
||||||
$entityManager->expects($this->once())
|
|
||||||
->method('persist');
|
|
||||||
$entityManager->expects($this->once())
|
|
||||||
->method('flush');
|
|
||||||
|
|
||||||
$serializer->expects($this->once())
|
|
||||||
->method('serialize')
|
|
||||||
->willReturn('test');
|
|
||||||
|
|
||||||
$response = $controller->restoreStoredObjectVersion($storedObjectVersion);
|
|
||||||
|
|
||||||
self::assertEquals(200, $response->getStatusCode());
|
|
||||||
self::assertEquals('test', $response->getContent());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testRestoreStoredObjectVersionAccessDenied(): void
|
|
||||||
{
|
|
||||||
$security = $this->createMock(Security::class);
|
|
||||||
$storedObjectRestore = $this->createMock(StoredObjectRestoreInterface::class);
|
|
||||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
|
||||||
$serializer = $this->createMock(SerializerInterface::class);
|
|
||||||
$storedObjectVersion = $this->createMock(StoredObjectVersion::class);
|
|
||||||
$controller = new StoredObjectRestoreVersionApiController($security, $storedObjectRestore, $entityManager, $serializer);
|
|
||||||
|
|
||||||
self::expectException(\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException::class);
|
|
||||||
$security->expects($this->once())
|
|
||||||
->method('isGranted')
|
|
||||||
->willReturn(false);
|
|
||||||
$controller->restoreStoredObjectVersion($storedObjectVersion);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,77 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Tests\Controller;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Controller\StoredObjectVersionApiController;
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
|
||||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
|
||||||
use Chill\MainBundle\Pagination\Paginator;
|
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
|
||||||
use Chill\MainBundle\Serializer\Normalizer\CollectionNormalizer;
|
|
||||||
use Prophecy\Argument;
|
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
|
||||||
use Symfony\Component\Serializer\Serializer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
|
|
||||||
{
|
|
||||||
use ProphecyTrait;
|
|
||||||
|
|
||||||
public function testListVersion(): void
|
|
||||||
{
|
|
||||||
$storedObject = new StoredObject();
|
|
||||||
for ($i = 0; $i < 15; ++$i) {
|
|
||||||
$storedObject->registerVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
|
|
||||||
->willReturn(true)
|
|
||||||
->shouldBeCalledOnce();
|
|
||||||
|
|
||||||
$controller = $this->buildController($security->reveal());
|
|
||||||
|
|
||||||
$response = $controller->listVersions($storedObject);
|
|
||||||
$body = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
|
||||||
|
|
||||||
self::assertEquals($response->getStatusCode(), 200);
|
|
||||||
self::assertIsArray($body);
|
|
||||||
self::assertArrayHasKey('results', $body);
|
|
||||||
self::assertCount(10, $body['results']);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildController(Security $security): StoredObjectVersionApiController
|
|
||||||
{
|
|
||||||
$paginator = $this->prophesize(Paginator::class);
|
|
||||||
$paginator->getCurrentPageFirstItemNumber()->willReturn(0);
|
|
||||||
$paginator->getItemsPerPage()->willReturn(10);
|
|
||||||
$paginator->getTotalItems()->willReturn(15);
|
|
||||||
$paginator->hasNextPage()->willReturn(false);
|
|
||||||
$paginator->hasPreviousPage()->willReturn(false);
|
|
||||||
|
|
||||||
$paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class);
|
|
||||||
$paginatorFactory->create(Argument::type('int'))->willReturn($paginator);
|
|
||||||
|
|
||||||
$serializer = new Serializer([
|
|
||||||
new StoredObjectVersionNormalizer(), new CollectionNormalizer(),
|
|
||||||
], [new JsonEncoder()]);
|
|
||||||
|
|
||||||
return new StoredObjectVersionApiController($paginatorFactory->reveal(), $serializer, $security);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
|
||||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectPointInTimeNormalizer;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
|
|
||||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Prophecy\Argument;
|
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
|
||||||
use Symfony\Component\Clock\MockClock;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
|
||||||
use Symfony\Component\Serializer\Serializer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class StoredObjectPointInTimeNormalizerTest extends TestCase
|
|
||||||
{
|
|
||||||
use ProphecyTrait;
|
|
||||||
|
|
||||||
public function testNormalize(): void
|
|
||||||
{
|
|
||||||
$storedObject = new StoredObject();
|
|
||||||
$version = $storedObject->registerVersion();
|
|
||||||
$storedObjectPointInTime = new StoredObjectPointInTime($version, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION, new User());
|
|
||||||
|
|
||||||
$normalizer = new StoredObjectPointInTimeNormalizer();
|
|
||||||
$normalizer->setNormalizer($this->buildNormalizer());
|
|
||||||
|
|
||||||
$actual = $normalizer->normalize($storedObjectPointInTime, 'json', ['read']);
|
|
||||||
|
|
||||||
self::assertIsArray($actual);
|
|
||||||
self::assertArrayHasKey('id', $actual);
|
|
||||||
self::assertArrayHasKey('byUser', $actual);
|
|
||||||
self::assertArrayHasKey('reason', $actual);
|
|
||||||
self::assertEquals(StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION->value, $actual['reason']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buildNormalizer(): NormalizerInterface
|
|
||||||
{
|
|
||||||
$userRender = $this->prophesize(UserRender::class);
|
|
||||||
$userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn('username');
|
|
||||||
|
|
||||||
return new Serializer(
|
|
||||||
[new UserNormalizer($userRender->reveal(), new MockClock())]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -20,12 +20,12 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
|
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Psr\Log\NullLogger;
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\Clock\MockClock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@@ -45,9 +45,6 @@ class PdfSignedMessageHandlerTest extends TestCase
|
|||||||
$entityWorkflow->setStep('new_step', $dto, 'new_transition', new \DateTimeImmutable(), new User());
|
$entityWorkflow->setStep('new_step', $dto, 'new_transition', new \DateTimeImmutable(), new User());
|
||||||
$step = $entityWorkflow->getCurrentStep();
|
$step = $entityWorkflow->getCurrentStep();
|
||||||
$signature = $step->getSignatures()->first();
|
$signature = $step->getSignatures()->first();
|
||||||
$stateChanger = $this->createMock(SignatureStepStateChanger::class);
|
|
||||||
$stateChanger->expects(self::once())->method('markSignatureAsSigned')
|
|
||||||
->with($signature, 99);
|
|
||||||
|
|
||||||
$handler = new PdfSignedMessageHandler(
|
$handler = new PdfSignedMessageHandler(
|
||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
@@ -55,12 +52,15 @@ class PdfSignedMessageHandlerTest extends TestCase
|
|||||||
$this->buildStoredObjectManager($storedObject, $expectedContent = '1234'),
|
$this->buildStoredObjectManager($storedObject, $expectedContent = '1234'),
|
||||||
$this->buildSignatureRepository($signature),
|
$this->buildSignatureRepository($signature),
|
||||||
$this->buildEntityManager(true),
|
$this->buildEntityManager(true),
|
||||||
$stateChanger,
|
new MockClock('now'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// we simply call the handler. The mocked StoredObjectManager will check that the "write" method is invoked once
|
// we simply call the handler. The mocked StoredObjectManager will check that the "write" method is invoked once
|
||||||
// with the content "1234"
|
// with the content "1234"
|
||||||
$handler(new PdfSignedMessage(10, 99, $expectedContent));
|
$handler(new PdfSignedMessage(10, 99, $expectedContent));
|
||||||
|
|
||||||
|
self::assertEquals('signed', $signature->getState()->value);
|
||||||
|
self::assertEquals(99, $signature->getZoneSignatureIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildSignatureRepository(EntityWorkflowStepSignature $signature): EntityWorkflowStepSignatureRepository
|
private function buildSignatureRepository(EntityWorkflowStepSignature $signature): EntityWorkflowStepSignatureRepository
|
||||||
|
@@ -1,53 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Tests\Service;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectRestore;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class StoredObjectRestoreTest extends TestCase
|
|
||||||
{
|
|
||||||
use ProphecyTrait;
|
|
||||||
|
|
||||||
public function testRestore(): void
|
|
||||||
{
|
|
||||||
$storedObject = new StoredObject();
|
|
||||||
$version = $storedObject->registerVersion(type: 'application/test');
|
|
||||||
|
|
||||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
|
||||||
$storedObjectManager->read($version)->willReturn('1234')->shouldBeCalledOnce();
|
|
||||||
$storedObjectManager->write($storedObject, '1234', 'application/test')->shouldBeCalledOnce()
|
|
||||||
->will(function ($args) {
|
|
||||||
/** @var StoredObject $object */
|
|
||||||
$object = $args[0];
|
|
||||||
|
|
||||||
return $object->registerVersion();
|
|
||||||
})
|
|
||||||
;
|
|
||||||
|
|
||||||
$restore = new StoredObjectRestore($storedObjectManager->reveal(), new NullLogger());
|
|
||||||
|
|
||||||
$newVersion = $restore->restore($version);
|
|
||||||
|
|
||||||
self::assertNotSame($version, $newVersion);
|
|
||||||
self::assertSame($version, $newVersion->getCreatedFrom());
|
|
||||||
}
|
|
||||||
}
|
|
@@ -105,50 +105,3 @@ paths:
|
|||||||
404:
|
404:
|
||||||
description: "Not found"
|
description: "Not found"
|
||||||
|
|
||||||
/1.0/doc-store/stored-object/{uuid}/versions:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- storedobject
|
|
||||||
summary: Get a signed route to post stored object
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: uuid
|
|
||||||
required: true
|
|
||||||
allowEmptyValue: false
|
|
||||||
description: The UUID of the storedObjeect
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "OK"
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
403:
|
|
||||||
description: "Unauthorized"
|
|
||||||
404:
|
|
||||||
description: "Not found"
|
|
||||||
|
|
||||||
/1.0/doc-store/stored-object/restore-from-version/{id}:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- storedobject
|
|
||||||
summary: Restore an old version of a stored object
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
allowEmptyValue: false
|
|
||||||
description: The id of the stored object version
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "OK"
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
|
|
||||||
|
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\Migrations\DocStore;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20240918073234 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Add a relation between stored object version when a version is restored';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version ADD createdFrom_id INT DEFAULT NULL');
|
|
||||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version ADD CONSTRAINT FK_C1D553024DEC38BB FOREIGN KEY (createdFrom_id) REFERENCES chill_doc.stored_object_version (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
|
||||||
$this->addSql('CREATE INDEX IDX_C1D553024DEC38BB ON chill_doc.stored_object_version (createdFrom_id)');
|
|
||||||
$this->addSql('ALTER INDEX chill_doc.idx_c1d55302232d562b RENAME TO IDX_C1D553024B136083');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version DROP createdFrom_id');
|
|
||||||
$this->addSql('ALTER INDEX chill_doc.idx_c1d553024b136083 RENAME TO idx_c1d55302232d562b');
|
|
||||||
}
|
|
||||||
}
|
|
@@ -94,38 +94,4 @@ 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('ROLE_USER');
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||||
$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,
|
||||||
$paginator->getItemsPerPage(),
|
$limit = $paginator->getItemsPerPage(),
|
||||||
$paginator->getCurrentPage()->getFirstItemNumber()
|
$offset = $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\FormInterface
|
* @return \Symfony\Component\Form\Form
|
||||||
*/
|
*/
|
||||||
private function passwordForm(User $user)
|
private function passwordForm(User $user)
|
||||||
{
|
{
|
||||||
|
@@ -264,7 +264,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +273,11 @@ class UserController extends CRUDController
|
|||||||
return parent::countEntities($action, $request, $filterOrder);
|
return parent::countEntities($action, $request, $filterOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->userRepository->countFilteredUsers($filterOrder->getQueryString(), $filterOrder->getCheckboxData('activeFilter'));
|
if (null === $filterOrder->getQueryString()) {
|
||||||
|
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
|
||||||
@@ -331,13 +334,16 @@ class UserController extends CRUDController
|
|||||||
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
$queryString = $filterOrder->getQueryString();
|
if (null === $filterOrder->getQueryString()) {
|
||||||
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||||
$nb = $this->userRepository->countFilteredUsers($queryString, $activeFilter);
|
}
|
||||||
|
|
||||||
$paginator = $this->getPaginatorFactory()->create($nb);
|
return $this->userRepository->findByUsernameOrEmail(
|
||||||
|
$filterOrder->getQueryString(),
|
||||||
return $this->userRepository->findFilteredUsers($queryString, $activeFilter, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage());
|
['usernameCanonical' => 'ASC'],
|
||||||
|
$paginator->getItemsPerPage(),
|
||||||
|
$paginator->getCurrentPageFirstItemNumber()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function onPrePersist(string $action, $entity, FormInterface $form, Request $request)
|
protected function onPrePersist(string $action, $entity, FormInterface $form, Request $request)
|
||||||
@@ -368,12 +374,10 @@ 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(
|
->setAction($this->generateUrl(
|
||||||
$this->generateUrl(
|
|
||||||
'admin_user_add_groupcenter',
|
'admin_user_add_groupcenter',
|
||||||
array_merge($returnPathParams, ['uid' => $user->getId()])
|
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'])
|
||||||
@@ -388,12 +392,10 @@ 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(
|
->setAction($this->generateUrl(
|
||||||
$this->generateUrl(
|
|
||||||
'admin_user_delete_groupcenter',
|
'admin_user_delete_groupcenter',
|
||||||
array_merge($returnPathParams, ['uid' => $user->getId(), 'gcid' => $groupCenter->getId()])
|
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();
|
||||||
|
@@ -12,18 +12,12 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Controller;
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable;
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
|
||||||
@@ -34,29 +28,13 @@ final readonly class WorkflowAddSignatureController
|
|||||||
private PDFSignatureZoneAvailable $PDFSignatureZoneAvailable,
|
private PDFSignatureZoneAvailable $PDFSignatureZoneAvailable,
|
||||||
private NormalizerInterface $normalizer,
|
private NormalizerInterface $normalizer,
|
||||||
private Environment $twig,
|
private Environment $twig,
|
||||||
private UrlGeneratorInterface $urlGenerator,
|
|
||||||
private Security $security,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route(path: '/{_locale}/main/workflow/signature/{id}/sign', name: 'chill_main_workflow_signature_add', methods: 'GET')]
|
#[Route(path: '/{_locale}/main/workflow/signature/{id}/sign', name: 'chill_main_workflow_signature_add', methods: 'GET')]
|
||||||
public function __invoke(EntityWorkflowStepSignature $signature, Request $request): Response
|
public function __invoke(EntityWorkflowStepSignature $signature, Request $request): Response
|
||||||
{
|
{
|
||||||
if (!$this->security->isGranted(EntityWorkflowStepSignatureVoter::SIGN, $signature)) {
|
|
||||||
throw new AccessDeniedHttpException('not authorized to sign this step');
|
|
||||||
}
|
|
||||||
|
|
||||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||||
|
|
||||||
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
|
|
||||||
if ($request->query->has('returnPath')) {
|
|
||||||
return new RedirectResponse($request->query->get('returnPath'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RedirectResponse(
|
|
||||||
$this->urlGenerator->generate('chill_main_workflow_show', ['id' => $entityWorkflow->getId()])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||||
if (null === $storedObject) {
|
if (null === $storedObject) {
|
||||||
throw new NotFoundHttpException('No stored object found');
|
throw new NotFoundHttpException('No stored object found');
|
||||||
|
@@ -396,10 +396,7 @@ class WorkflowController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($signature->getSigner() instanceof User) {
|
if ($signature->getSigner() instanceof User) {
|
||||||
return $this->redirectToRoute('chill_main_workflow_signature_add', [
|
return $this->redirectToRoute('chill_main_workflow_signature_add', ['id' => $signature_id]);
|
||||||
'id' => $signature_id,
|
|
||||||
'returnPath' => $request->query->get('returnPath', null),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
|
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
|
||||||
@@ -423,10 +420,7 @@ class WorkflowController extends AbstractController
|
|||||||
$this->entityManager->persist($signature);
|
$this->entityManager->persist($signature);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
return $this->redirectToRoute('chill_main_workflow_signature_add', [
|
return $this->redirectToRoute('chill_main_workflow_signature_add', ['id' => $signature_id]);
|
||||||
'id' => $signature_id,
|
|
||||||
'returnPath' => $request->query->get('returnPath', null),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render(
|
return $this->render(
|
||||||
|
@@ -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<string|int, int|float|string|bool|array<int|string, int|float|string|bool>> $lastExecutionData the data which was returned from the previous execution
|
* @param array $lastExecutionData the data which was returned from 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
|
* @return array|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;
|
||||||
}
|
}
|
||||||
|
@@ -318,7 +318,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return array_values($usersInvolved);
|
return $usersInvolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getWorkflowName(): string
|
public function getWorkflowName(): string
|
||||||
@@ -446,10 +446,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
$newStep->addDestUser($user);
|
$newStep->addDestUser($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $transitionContextDTO->futureUserSignature) {
|
|
||||||
$newStep->addDestUser($transitionContextDTO->futureUserSignature);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($transitionContextDTO->futureDestEmails as $email) {
|
foreach ($transitionContextDTO->futureDestEmails as $email) {
|
||||||
$newStep->addDestEmail($email);
|
$newStep->addDestEmail($email);
|
||||||
}
|
}
|
||||||
|
@@ -105,11 +105,6 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate
|
|||||||
return $this->state;
|
return $this->state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return $this
|
|
||||||
*
|
|
||||||
* @internal You should not use this method directly, use @see{Chill\MainBundle\Workflow\SignatureStepStateChanger} instead
|
|
||||||
*/
|
|
||||||
public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature
|
public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature
|
||||||
{
|
{
|
||||||
$this->state = $state;
|
$this->state = $state;
|
||||||
@@ -122,11 +117,6 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate
|
|||||||
return $this->stateDate;
|
return $this->stateDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return $this
|
|
||||||
*
|
|
||||||
* @internal You should not use this method directly, use @see{Chill\MainBundle\Workflow\SignatureStepStateChanger} instead
|
|
||||||
*/
|
|
||||||
public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature
|
public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature
|
||||||
{
|
{
|
||||||
$this->stateDate = $stateDate;
|
$this->stateDate = $stateDate;
|
||||||
@@ -139,11 +129,6 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate
|
|||||||
return $this->zoneSignatureIndex;
|
return $this->zoneSignatureIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return $this
|
|
||||||
*
|
|
||||||
* @internal You should not use this method directly, use @see{Chill\MainBundle\Workflow\SignatureStepStateChanger} instead
|
|
||||||
*/
|
|
||||||
public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature
|
public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature
|
||||||
{
|
{
|
||||||
$this->zoneSignatureIndex = $zoneSignatureIndex;
|
$this->zoneSignatureIndex = $zoneSignatureIndex;
|
||||||
@@ -156,32 +141,6 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate
|
|||||||
return EntityWorkflowSignatureStateEnum::SIGNED == $this->getState();
|
return EntityWorkflowSignatureStateEnum::SIGNED == $this->getState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isPending(): bool
|
|
||||||
{
|
|
||||||
return EntityWorkflowSignatureStateEnum::PENDING == $this->getState();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether all signatures associated with a given workflow step are not pending.
|
|
||||||
*
|
|
||||||
* Iterates over each signature in the provided workflow step, and returns false if any signature
|
|
||||||
* is found to be pending. If all signatures are not pending, returns true.
|
|
||||||
*
|
|
||||||
* @param EntityWorkflowStep $step the workflow step whose signatures are to be checked
|
|
||||||
*
|
|
||||||
* @return bool true if all signatures are not pending, false otherwise
|
|
||||||
*/
|
|
||||||
public static function isAllSignatureNotPendingForStep(EntityWorkflowStep $step): bool
|
|
||||||
{
|
|
||||||
foreach ($step->getSignatures() as $signature) {
|
|
||||||
if ($signature->isPending()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return 'person'|'user'
|
* @return 'person'|'user'
|
||||||
*/
|
*/
|
||||||
|
@@ -13,8 +13,6 @@ 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;
|
||||||
@@ -83,7 +81,10 @@ final class NotificationRepository implements ObjectRepository
|
|||||||
$results->free();
|
$results->free();
|
||||||
} else {
|
} else {
|
||||||
$wheres = [];
|
$wheres = [];
|
||||||
foreach ([['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId], ...$more] as $k => ['relatedEntityClass' => $relClass, 'relatedEntityId' => $relId]) {
|
foreach ([
|
||||||
|
['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;
|
||||||
@@ -254,12 +255,10 @@ 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(
|
$or = $qb->expr()->orX($qb->expr()->andX(
|
||||||
$qb->expr()->andX(
|
|
||||||
$qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'),
|
$qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'),
|
||||||
$qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')
|
$qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')
|
||||||
)
|
));
|
||||||
);
|
|
||||||
$qb
|
$qb
|
||||||
->setParameter('relatedEntityClass', $relatedEntityClass)
|
->setParameter('relatedEntityClass', $relatedEntityClass)
|
||||||
->setParameter('relatedEntityId', $relatedEntityId);
|
->setParameter('relatedEntityId', $relatedEntityId);
|
||||||
@@ -311,86 +310,4 @@ 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,7 +17,6 @@ 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;
|
||||||
@@ -27,25 +26,9 @@ final readonly class UserRepository implements UserRepositoryInterface
|
|||||||
{
|
{
|
||||||
private EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
private const FIELDS = [
|
private const FIELDS = ['id', 'email', 'enabled', 'civility_id', 'civility_abbreviation', 'civility_name', 'label', 'mainCenter_id',
|
||||||
'id',
|
'mainCenter_name', 'mainScope_id', 'mainScope_name', 'userJob_id', 'userJob_name', 'currentLocation_id', 'currentLocation_name',
|
||||||
'email',
|
'mainLocation_id', 'mainLocation_name'];
|
||||||
'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)
|
||||||
{
|
{
|
||||||
@@ -313,25 +296,6 @@ 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');
|
||||||
@@ -348,49 +312,4 @@ 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,13 +1,11 @@
|
|||||||
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
|
document.querySelectorAll('.notification_toggle_read_status')
|
||||||
.querySelectorAll(".notification_toggle_read_status")
|
|
||||||
.forEach(function (el, i) {
|
.forEach(function (el, i) {
|
||||||
createApp({
|
createApp({
|
||||||
template: `<notification-read-toggle
|
template: `<notification-read-toggle
|
||||||
@@ -24,40 +22,35 @@ window.addEventListener("DOMContentLoaded", function (e) {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
notificationId: parseInt(el.dataset.notificationId),
|
notificationId: 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 {
|
} else { throw 'data-container attribute is missing' }
|
||||||
throw "data-container attribute is missing";
|
|
||||||
}
|
|
||||||
this.isRead = false;
|
this.isRead = false;
|
||||||
},
|
},
|
||||||
onMarkUnread() {
|
onMarkUnread() {
|
||||||
if (typeof this.getContainer[i] !== "undefined") {
|
if (typeof this.getContainer[i] !== 'undefined') {
|
||||||
this.getContainer[i].classList.replace("unread", "read");
|
this.getContainer[i].classList.replace('unread', 'read');
|
||||||
} else {
|
} else { throw 'data-container attribute is missing' }
|
||||||
throw "data-container attribute is missing";
|
|
||||||
}
|
|
||||||
this.isRead = true;
|
this.isRead = true;
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
.use(i18n)
|
.use(i18n)
|
||||||
.mount(el);
|
.mount(el);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,39 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,50 +0,0 @@
|
|||||||
<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,11 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div :class="{'btn-group btn-group-sm float-end': isButtonGroup }"
|
||||||
:class="{ 'btn-group btn-group-sm float-end': isButtonGroup }"
|
role="group" aria-label="Notification actions">
|
||||||
role="group"
|
|
||||||
aria-label="Notification actions"
|
<button v-if="isRead"
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="isRead"
|
|
||||||
class="btn"
|
class="btn"
|
||||||
:class="overrideClass"
|
:class="overrideClass"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -14,12 +11,11 @@
|
|||||||
>
|
>
|
||||||
<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
|
<button v-if="!isRead"
|
||||||
v-if="!isRead"
|
|
||||||
class="btn"
|
class="btn"
|
||||||
:class="overrideClass"
|
:class="overrideClass"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -28,12 +24,11 @@
|
|||||||
>
|
>
|
||||||
<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
|
<a v-if="isButtonGroup"
|
||||||
v-if="isButtonGroup"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-primary"
|
class="btn btn-outline-primary"
|
||||||
:href="showUrl"
|
:href="showUrl"
|
||||||
@@ -42,25 +37,11 @@
|
|||||||
<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",
|
||||||
@@ -76,7 +57,7 @@ export default {
|
|||||||
// Optional
|
// Optional
|
||||||
buttonClass: {
|
buttonClass: {
|
||||||
required: false,
|
required: false,
|
||||||
type: String,
|
type: String
|
||||||
},
|
},
|
||||||
buttonNoText: {
|
buttonNoText: {
|
||||||
required: false,
|
required: false,
|
||||||
@@ -84,14 +65,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() {
|
||||||
@@ -101,48 +82,31 @@ 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(
|
makeFetch('POST', `/api/1.0/main/notification/${this.notificationId}/mark/unread`, []).then(response => {
|
||||||
"POST",
|
this.$emit('markRead', { notificationId: this.notificationId });
|
||||||
`/api/1.0/main/notification/${this.notificationId}/mark/unread`,
|
})
|
||||||
[]
|
|
||||||
).then((response) => {
|
|
||||||
this.$emit("markRead", {notificationId: this.notificationId});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
markAsRead() {
|
markAsRead() {
|
||||||
makeFetch(
|
makeFetch('POST', `/api/1.0/main/notification/${this.notificationId}/mark/read`, []).then(response => {
|
||||||
"POST",
|
this.$emit('markUnread', { notificationId: this.notificationId });
|
||||||
`/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>
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
|
@@ -1,16 +1,13 @@
|
|||||||
{% 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
|
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">
|
||||||
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">
|
||||||
@@ -19,7 +16,7 @@
|
|||||||
<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 %}
|
||||||
@@ -27,7 +24,7 @@
|
|||||||
{{ 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 %}
|
||||||
@@ -37,20 +34,20 @@
|
|||||||
{% 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 %}
|
||||||
@@ -60,10 +57,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for a in c.notification.addressesEmails %}
|
{% for a in c.notification.addressesEmails %}
|
||||||
<span
|
<span class="badge-user" title="{{ 'notification.Email with access link'|trans|e('html_attr') }}">
|
||||||
class="badge-user"
|
|
||||||
title="{{ 'notification.Email with access link'|trans|e('html_attr') }}"
|
|
||||||
>
|
|
||||||
{{ a }}
|
{{ a }}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -76,6 +70,7 @@
|
|||||||
</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 %}
|
||||||
@@ -90,29 +85,25 @@
|
|||||||
{% 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">
|
<p class="read-more"><a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">{{ 'Read more'|trans }}</a></p>
|
||||||
<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">
|
||||||
@@ -120,13 +111,13 @@
|
|||||||
</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
|
<span class="notification_toggle_read_status"
|
||||||
class="notification_toggle_read_status"
|
|
||||||
data-notification-id="{{ c.notification.id }}"
|
data-notification-id="{{ c.notification.id }}"
|
||||||
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
|
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
|
||||||
data-container="notification-status"
|
data-container="notification-status"
|
||||||
@@ -134,31 +125,18 @@
|
|||||||
</li>
|
</li>
|
||||||
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %}
|
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="{{ chill_path_add_return_path('chill_main_notification_edit', {'id': c.notification.id}) }}"
|
||||||
href="{{ chill_path_add_return_path(
|
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
||||||
'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',
|
{% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', c.notification) %}
|
||||||
c.notification) %}
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}"
|
||||||
href="{{ chill_path_add_return_path(
|
class="btn {% if not c.notification.isSystem %}btn-show change-icon{% else %}btn-misc{% endif %}" title="{{ 'notification.see_comments_thread'|trans }}">
|
||||||
'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>
|
||||||
@@ -169,30 +147,24 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
<div
|
<div class="item-bloc notification-status {% if notification.isReadBy(app.user) %}read{% else %}unread{% endif %}">
|
||||||
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
|
<button type="button" class="accordion-button collapsed"
|
||||||
type="button"
|
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ notification.id }}"
|
||||||
class="accordion-button collapsed"
|
aria-expanded="false" aria-controls="flush-collapse-{{ notification.id }}">
|
||||||
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
|
<div id="flush-collapse-{{ notification.id }}"
|
||||||
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) }}
|
||||||
@@ -202,4 +174,5 @@
|
|||||||
{{ _self.content(_context) }}
|
{{ _self.content(_context) }}
|
||||||
{{ _self.actions(_context) }}
|
{{ _self.actions(_context) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -4,75 +4,59 @@
|
|||||||
|
|
||||||
{% 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">
|
<ul class="nav nav-pills justify-content-center">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a class="nav-link {% if step == 'inbox' %}active{% endif %}" href="{{ path('chill_main_notification_my') }}">
|
||||||
class="nav-link {% if step == 'inbox' %}active{% endif %}"
|
{{ 'notification.Notifications received'|trans }}
|
||||||
href="{{ path('chill_main_notification_my') }}"
|
|
||||||
>
|
|
||||||
{{ "notification.Notifications received" | trans }}
|
|
||||||
{% if unreads['inbox'] > 0 %}
|
{% if unreads['inbox'] > 0 %}
|
||||||
<span class="badge rounded-pill bg-danger">
|
<span class="badge rounded-pill bg-danger">
|
||||||
{{ unreads["inbox"] }}
|
{{ unreads['inbox'] }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a class="nav-link {% if step == 'sent' %}active{% endif %}" href="{{ path('chill_main_notification_sent') }}">
|
||||||
class="nav-link {% if step == 'sent' %}active{% endif %}"
|
{{ 'notification.Notifications sent'|trans }}
|
||||||
href="{{ path('chill_main_notification_sent') }}"
|
|
||||||
>
|
|
||||||
{{ "notification.Notifications sent" | trans }}
|
|
||||||
{% if unreads['sent'] > 0 %}
|
{% if unreads['sent'] > 0 %}
|
||||||
<span class="badge rounded-pill bg-danger">
|
<span class="badge rounded-pill bg-danger">
|
||||||
{{ unreads["sent"] }}
|
{{ unreads['sent'] }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{% if datas|length == 0 %} {% if step == 'inbox' %}
|
{% if datas|length == 0 %}
|
||||||
<p class="chill-no-data-statement">
|
{% if step == 'inbox' %}
|
||||||
{{ "notification.Any notification received" | trans }}
|
<p class="chill-no-data-statement">{{ 'notification.Any notification received'|trans }}</p>
|
||||||
</p>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="chill-no-data-statement">
|
<p class="chill-no-data-statement">{{ 'notification.Any notification sent'|trans }}</p>
|
||||||
{{ "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, 'notification_cc': data.template_data.notificationCc
|
'fold_item': true,
|
||||||
is defined ? data.template_data.notificationCc : false } %}
|
'notification_cc': data.template_data.notificationCc is defined ? data.template_data.notificationCc : false
|
||||||
|
} %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ chill_pagination(paginator) }}
|
{{ chill_pagination(paginator) }}
|
||||||
{% endif %}
|
{% 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>
|
</div>
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@@ -40,13 +40,11 @@
|
|||||||
{% set forward = workflow_metadata(step.entityWorkflow, 'isForward', transition) %}
|
{% set forward = workflow_metadata(step.entityWorkflow, 'isForward', transition) %}
|
||||||
<div class="item-row separator">
|
<div class="item-row separator">
|
||||||
<div class="item-col" style="width: inherit;">
|
<div class="item-col" style="width: inherit;">
|
||||||
|
{% if step.transitionBy is not null %}
|
||||||
<div>
|
<div>
|
||||||
{%- if step.transitionBy is not null -%}
|
|
||||||
{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}
|
{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}
|
||||||
{% else %}
|
|
||||||
<span class="chill-no-data-statement">{{ 'workflow.Automated transition'|trans }}</span>
|
|
||||||
{%- endif -%}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div>
|
<div>
|
||||||
<span>{{ step.transitionAt|format_datetime('long', 'medium') }}</span>
|
<span>{{ step.transitionAt|format_datetime('long', 'medium') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -23,7 +23,6 @@
|
|||||||
{% if s.isSigned %}
|
{% if s.isSigned %}
|
||||||
<span class="text-end">{{ 'workflow.signature_zone.has_signed_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
|
<span class="text-end">{{ 'workflow.signature_zone.has_signed_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s) %}
|
|
||||||
<ul class="record_actions slim">
|
<ul class="record_actions slim">
|
||||||
<li>
|
<li>
|
||||||
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a>
|
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a>
|
||||||
@@ -32,9 +31,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
|
||||||
<span class="text-end">{{ 'workflow.waiting_for_signature'|trans }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -25,11 +25,13 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<div class="content" id="content">
|
<div class="content" id="content">
|
||||||
|
<div class="container-xxl">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 m-auto">
|
<div class="col-xs-12 col-md-12 col-lg-9 my-5 m-auto">
|
||||||
<div class="row" id="document-signature"></div>
|
<div class="row" id="document-signature"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@@ -3,28 +3,24 @@
|
|||||||
{% if step.previous is not null %}
|
{% if step.previous is not null %}
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
|
||||||
<b>{% if step.previous.transitionBy is not null %}{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}{% else %}<span class="chill-no-data-statement">{{ 'workflow.Automated transition'|trans }}</span>{% endif %}</b>
|
<b>{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}</b>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
|
||||||
<b>{{ step.previous.transitionAt|format_datetime('short', 'short') }}</b>
|
<b>{{ step.previous.transitionAt|format_datetime('short', 'short') }}</b>
|
||||||
</li>
|
</li>
|
||||||
{% if step.destUser|length > 0 %}
|
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
|
||||||
<b>
|
<b>
|
||||||
{% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
{% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||||
</b>
|
</b>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
|
||||||
{% if step.ccUser|length > 0 %}
|
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span>
|
||||||
<b>
|
<b>
|
||||||
{% for u in step.ccUser %}{{ u|chill_entity_render_string({'at_date': step.previous.transitionAt }) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
{% for u in step.ccUser %}{{ u|chill_entity_render_string({'at_date': step.previous.transitionAt }) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||||
</b>
|
</b>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>
|
||||||
|
@@ -8,6 +8,6 @@ Vous êtes invités à valider cette étape au plus tôt.
|
|||||||
|
|
||||||
Vous pouvez visualiser le workflow sur cette page:
|
Vous pouvez visualiser le workflow sur cette page:
|
||||||
|
|
||||||
{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id, '_locale': 'fr'})) }}
|
{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id})) }}
|
||||||
|
|
||||||
Cordialement,
|
Cordialement,
|
||||||
|
@@ -6,7 +6,7 @@ Titre du workflow: "{{ entityTitle }}".
|
|||||||
|
|
||||||
Vous êtes invité·e à valider cette étape. Pour obtenir un accès, vous pouvez cliquer sur le lien suivant:
|
Vous êtes invité·e à valider cette étape. Pour obtenir un accès, vous pouvez cliquer sur le lien suivant:
|
||||||
|
|
||||||
{{ absolute_url(path('chill_main_workflow_grant_access_by_key', {'id': entity_workflow.currentStep.id, 'accessKey': entity_workflow.currentStep.accessKey, '_locale': fr})) }}
|
{{ absolute_url(path('chill_main_workflow_grant_access_by_key', {'id': entity_workflow.currentStep.id, 'accessKey': entity_workflow.currentStep.accessKey})) }}
|
||||||
|
|
||||||
Dès que vous aurez cliqué une fois sur le lien, vous serez autorisé à valider cette étape.
|
Dès que vous aurez cliqué une fois sur le lien, vous serez autorisé à valider cette étape.
|
||||||
|
|
||||||
|
@@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Security\Authorization;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
|
||||||
use Chill\PersonBundle\Entity\Person;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|
||||||
|
|
||||||
final class EntityWorkflowStepSignatureVoter extends Voter
|
|
||||||
{
|
|
||||||
public const SIGN = 'CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN';
|
|
||||||
|
|
||||||
protected function supports(string $attribute, $subject)
|
|
||||||
{
|
|
||||||
return $subject instanceof EntityWorkflowStepSignature && self::SIGN === $attribute;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
|
|
||||||
{
|
|
||||||
/** @var EntityWorkflowStepSignature $subject */
|
|
||||||
if ($subject->getSigner() instanceof Person) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($subject->getSigner() === $token->getUser()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,88 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Security\Authorization;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
|
||||||
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
|
||||||
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
||||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A voter class that determines if a user has permission to apply all transitions
|
|
||||||
* in a workflow based on their roles and the centers they have access to.
|
|
||||||
*/
|
|
||||||
final class EntityWorkflowTransitionVoter extends Voter implements ProvideRoleHierarchyInterface
|
|
||||||
{
|
|
||||||
final public const APPLY_ALL_TRANSITIONS = 'CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION';
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly EntityWorkflowManager $workflowManager,
|
|
||||||
private readonly AuthorizationHelperForCurrentUserInterface $authorizationHelper,
|
|
||||||
private readonly CenterResolverManagerInterface $centerResolverManager,
|
|
||||||
private readonly AccessDecisionManagerInterface $accessDecisionManager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function getRoles(): array
|
|
||||||
{
|
|
||||||
return [self::APPLY_ALL_TRANSITIONS];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRolesWithoutScope(): array
|
|
||||||
{
|
|
||||||
return [self::APPLY_ALL_TRANSITIONS];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRolesWithHierarchy(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'workflow.Permissions' => [
|
|
||||||
self::APPLY_ALL_TRANSITIONS,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function supports(string $attribute, $subject): bool
|
|
||||||
{
|
|
||||||
return self::APPLY_ALL_TRANSITIONS === $attribute && $subject instanceof EntityWorkflowStep;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
|
|
||||||
{
|
|
||||||
/** @var EntityWorkflowStep $subject */
|
|
||||||
$entityWorkflow = $subject->getEntityWorkflow();
|
|
||||||
|
|
||||||
if (!$this->accessDecisionManager->decide($token, [EntityWorkflowVoter::SEE], $entityWorkflow)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$handler = $this->workflowManager->getHandler($entityWorkflow);
|
|
||||||
$entity = $handler->getRelatedEntity($entityWorkflow);
|
|
||||||
|
|
||||||
if (null === $entity) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$centers = $this->centerResolverManager->resolveCenters($entity);
|
|
||||||
$reachableCenters = $this->authorizationHelper->getReachableCenters(self::APPLY_ALL_TRANSITIONS);
|
|
||||||
|
|
||||||
foreach ($centers as $center) {
|
|
||||||
if (in_array($center, $reachableCenters, true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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->expire($key, $ttl);
|
$this->chillRedis->setTimeout($key, $ttl);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@@ -15,12 +15,6 @@ 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'
|
||||||
@@ -53,18 +47,11 @@ 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($allowRemoveDoubleRefId);
|
$this->updateAddressReferenceTable();
|
||||||
|
|
||||||
$this->deleteTemporaryTable();
|
$this->deleteTemporaryTable();
|
||||||
|
|
||||||
@@ -72,11 +59,6 @@ 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,
|
||||||
@@ -185,48 +167,15 @@ final class AddressReferenceBaseImporter
|
|||||||
$this->isInitialized = true;
|
$this->isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateAddressReferenceTable(bool $allowRemoveDoubleRefId): void
|
private function updateAddressReferenceTable(): 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)'
|
||||||
);
|
);
|
||||||
|
|
||||||
// 0) detect for doublon in current temporary table
|
|
||||||
$results = $this->defaultConnection->executeQuery(
|
|
||||||
'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
|
// 1) Add new addresses
|
||||||
$this->logger->info(self::LOG_PREFIX.'upsert new addresses');
|
$this->logger->info(self::LOG_PREFIX.'upsert new addresses');
|
||||||
$affected = $connection->executeStatement("INSERT INTO chill_main_address_reference
|
$affected = $this->defaultConnection->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'),
|
||||||
@@ -248,13 +197,12 @@ final class AddressReferenceBaseImporter
|
|||||||
|
|
||||||
// 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 = $connection->executeStatement('UPDATE chill_main_address_reference
|
$affected = $this->defaultConnection->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,8 +42,7 @@ class PostalCodeBaseImporter
|
|||||||
NOW(),
|
NOW(),
|
||||||
NOW()
|
NOW()
|
||||||
FROM g
|
FROM g
|
||||||
ON CONFLICT (code, refpostalcodeid, postalcodeSource) WHERE refpostalcodeid IS NOT NULL DO UPDATE
|
ON CONFLICT (code, refpostalcodeid, postalcodeSource) WHERE refpostalcodeid IS NOT NULL DO UPDATE SET label = excluded.label, center = excluded.center, updatedAt = NOW()
|
||||||
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 = '(?, ?, ?, ?, ?, ?, ?, ?)';
|
||||||
|
@@ -19,7 +19,24 @@ use Twig\TwigFilter;
|
|||||||
*/
|
*/
|
||||||
class ChillEntityRenderExtension extends AbstractExtension
|
class ChillEntityRenderExtension extends AbstractExtension
|
||||||
{
|
{
|
||||||
public function __construct(private readonly ChillEntityRenderManagerInterface $renderManager) {}
|
/**
|
||||||
|
* @var ChillEntityRender
|
||||||
|
*/
|
||||||
|
protected $defaultRender;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var iterable|ChillEntityRenderInterface[]
|
||||||
|
*/
|
||||||
|
protected $renders = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChillEntityRenderExtension constructor.
|
||||||
|
*/
|
||||||
|
public function __construct(iterable $renders)
|
||||||
|
{
|
||||||
|
$this->defaultRender = new ChillEntityRender();
|
||||||
|
$this->renders = $renders;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array|TwigFilter[]
|
* @return array|TwigFilter[]
|
||||||
@@ -36,13 +53,34 @@ class ChillEntityRenderExtension extends AbstractExtension
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function renderBox(?object $entity, array $options = []): string
|
public function renderBox($entity, array $options = []): string
|
||||||
{
|
{
|
||||||
return $this->renderManager->renderBox($entity, $options);
|
if (null === $entity) {
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function renderString(?object $entity, array $options = []): string
|
return $this->getRender($entity, $options)
|
||||||
|
->renderBox($entity, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderString($entity, array $options = []): string
|
||||||
{
|
{
|
||||||
return $this->renderManager->renderString($entity, $options);
|
if (null === $entity) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getRender($entity, $options)
|
||||||
|
->renderString($entity, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRender($entity, $options): ?ChillEntityRenderInterface
|
||||||
|
{
|
||||||
|
foreach ($this->renders as $render) {
|
||||||
|
if ($render->supports($entity, $options)) {
|
||||||
|
return $render;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->defaultRender;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@ namespace Chill\MainBundle\Templating\Entity;
|
|||||||
* Interface to implement which will render an entity in template on a custom
|
* Interface to implement which will render an entity in template on a custom
|
||||||
* manner.
|
* manner.
|
||||||
*
|
*
|
||||||
* @template T of object
|
* @template T
|
||||||
*/
|
*/
|
||||||
interface ChillEntityRenderInterface
|
interface ChillEntityRenderInterface
|
||||||
{
|
{
|
||||||
@@ -31,7 +31,7 @@ interface ChillEntityRenderInterface
|
|||||||
* </span>
|
* </span>
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param T|null $entity
|
* @param T $entity
|
||||||
*
|
*
|
||||||
* @phpstan-pure
|
* @phpstan-pure
|
||||||
*/
|
*/
|
||||||
@@ -42,7 +42,7 @@ interface ChillEntityRenderInterface
|
|||||||
*
|
*
|
||||||
* Example: returning the name of a person.
|
* Example: returning the name of a person.
|
||||||
*
|
*
|
||||||
* @param T|null $entity
|
* @param T $entity
|
||||||
*
|
*
|
||||||
* @phpstan-pure
|
* @phpstan-pure
|
||||||
*/
|
*/
|
||||||
|
@@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Templating\Entity;
|
|
||||||
|
|
||||||
final readonly class ChillEntityRenderManager implements ChillEntityRenderManagerInterface
|
|
||||||
{
|
|
||||||
private ChillEntityRender $defaultRender;
|
|
||||||
|
|
||||||
public function __construct(/**
|
|
||||||
* @var iterable<ChillEntityRenderInterface>
|
|
||||||
*/
|
|
||||||
private iterable $renders,
|
|
||||||
) {
|
|
||||||
$this->defaultRender = new ChillEntityRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function renderBox($entity, array $options = []): string
|
|
||||||
{
|
|
||||||
if (null === $entity) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->getRender($entity, $options)
|
|
||||||
->renderBox($entity, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function renderString($entity, array $options = []): string
|
|
||||||
{
|
|
||||||
if (null === $entity) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->getRender($entity, $options)
|
|
||||||
->renderString($entity, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getRender($entity, $options): ChillEntityRenderInterface
|
|
||||||
{
|
|
||||||
foreach ($this->renders as $render) {
|
|
||||||
if ($render->supports($entity, $options)) {
|
|
||||||
return $render;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->defaultRender;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Templating\Entity;
|
|
||||||
|
|
||||||
interface ChillEntityRenderManagerInterface
|
|
||||||
{
|
|
||||||
public function renderBox(?object $entity, array $options = []): string;
|
|
||||||
|
|
||||||
public function renderString(?object $entity, array $options = []): string;
|
|
||||||
}
|
|
@@ -1,144 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Tests\Authorization;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Center;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowTransitionVoter;
|
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
|
||||||
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
|
||||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class EntityWorkflowTransitionVoterTest extends TestCase
|
|
||||||
{
|
|
||||||
use ProphecyTrait;
|
|
||||||
|
|
||||||
public function testVoteOnAttributeHappyScenario(): void
|
|
||||||
{
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$object = new \stdClass();
|
|
||||||
$center = new Center();
|
|
||||||
$user = new User();
|
|
||||||
|
|
||||||
$handler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
|
||||||
$handler->getRelatedEntity($entityWorkflow)->willReturn($object);
|
|
||||||
|
|
||||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
|
||||||
$entityWorkflowManager->getHandler($entityWorkflow)->willReturn($handler);
|
|
||||||
|
|
||||||
$centerResolver = $this->prophesize(CenterResolverManagerInterface::class);
|
|
||||||
$centerResolver->resolveCenters($object)->willReturn([$center, new Center()]);
|
|
||||||
|
|
||||||
$autorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
|
|
||||||
$autorizationHelper->getReachableCenters('CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION')
|
|
||||||
->willReturn([$center, new Center()]);
|
|
||||||
|
|
||||||
$token = new UsernamePasswordToken($user, 'default', $user->getRoles());
|
|
||||||
|
|
||||||
$accessDecision = $this->prophesize(AccessDecisionManagerInterface::class);
|
|
||||||
$accessDecision->decide($token, [EntityWorkflowVoter::SEE], $entityWorkflow)
|
|
||||||
->willReturn(true)->shouldBeCalled();
|
|
||||||
|
|
||||||
$voter = new EntityWorkflowTransitionVoter(
|
|
||||||
$entityWorkflowManager->reveal(),
|
|
||||||
$autorizationHelper->reveal(),
|
|
||||||
$centerResolver->reveal(),
|
|
||||||
$accessDecision->reveal(),
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertEquals(Voter::ACCESS_GRANTED, $voter->vote($token, $entityWorkflow->getCurrentStep(), ['CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION']));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testVoteOnAttributeCenterNotReachable(): void
|
|
||||||
{
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$object = new \stdClass();
|
|
||||||
$user = new User();
|
|
||||||
|
|
||||||
$handler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
|
||||||
$handler->getRelatedEntity($entityWorkflow)->willReturn($object);
|
|
||||||
|
|
||||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
|
||||||
$entityWorkflowManager->getHandler($entityWorkflow)->willReturn($handler);
|
|
||||||
|
|
||||||
$centerResolver = $this->prophesize(CenterResolverManagerInterface::class);
|
|
||||||
$centerResolver->resolveCenters($object)->willReturn([new Center()]);
|
|
||||||
|
|
||||||
$autorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
|
|
||||||
$autorizationHelper->getReachableCenters('CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION')
|
|
||||||
->willReturn([new Center()]);
|
|
||||||
|
|
||||||
$token = new UsernamePasswordToken($user, 'default', $user->getRoles());
|
|
||||||
|
|
||||||
$accessDecision = $this->prophesize(AccessDecisionManagerInterface::class);
|
|
||||||
$accessDecision->decide($token, [EntityWorkflowVoter::SEE], $entityWorkflow)
|
|
||||||
->willReturn(true)->shouldBeCalled();
|
|
||||||
|
|
||||||
$voter = new EntityWorkflowTransitionVoter(
|
|
||||||
$entityWorkflowManager->reveal(),
|
|
||||||
$autorizationHelper->reveal(),
|
|
||||||
$centerResolver->reveal(),
|
|
||||||
$accessDecision->reveal(),
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertEquals(Voter::ACCESS_DENIED, $voter->vote($token, $entityWorkflow->getCurrentStep(), ['CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION']));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testVoteNotOnSupportedAttribute(): void
|
|
||||||
{
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$object = new \stdClass();
|
|
||||||
$user = new User();
|
|
||||||
|
|
||||||
$handler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
|
||||||
$handler->getRelatedEntity($entityWorkflow)->willReturn($object);
|
|
||||||
|
|
||||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
|
||||||
$entityWorkflowManager->getHandler($entityWorkflow)->willReturn($handler);
|
|
||||||
|
|
||||||
$centerResolver = $this->prophesize(CenterResolverManagerInterface::class);
|
|
||||||
$centerResolver->resolveCenters($object)->willReturn([new Center()]);
|
|
||||||
|
|
||||||
$autorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
|
|
||||||
$autorizationHelper->getReachableCenters('CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION')
|
|
||||||
->willReturn([new Center()]);
|
|
||||||
|
|
||||||
$token = new UsernamePasswordToken($user, 'default', $user->getRoles());
|
|
||||||
|
|
||||||
$accessDecision = $this->prophesize(AccessDecisionManagerInterface::class);
|
|
||||||
$accessDecision->decide($token, [EntityWorkflowVoter::SEE], $entityWorkflow)
|
|
||||||
->willReturn(true);
|
|
||||||
|
|
||||||
$voter = new EntityWorkflowTransitionVoter(
|
|
||||||
$entityWorkflowManager->reveal(),
|
|
||||||
$autorizationHelper->reveal(),
|
|
||||||
$centerResolver->reveal(),
|
|
||||||
$accessDecision->reveal(),
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertEquals(Voter::ACCESS_ABSTAIN, $voter->vote($token, new \stdClass(), ['CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION']));
|
|
||||||
self::assertEquals(Voter::ACCESS_ABSTAIN, $voter->vote($token, $entityWorkflow->getCurrentStep(), ['SOMETHING_ELSE']));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -18,14 +18,11 @@ use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable;
|
|||||||
use Chill\MainBundle\Controller\WorkflowAddSignatureController;
|
use Chill\MainBundle\Controller\WorkflowAddSignatureController;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
|
||||||
@@ -65,13 +62,7 @@ class WorkflowAddSignatureControllerTest extends TestCase
|
|||||||
$twig->method('render')->with('@ChillMain/Workflow/_signature_sign.html.twig', $this->isType('array'))
|
$twig->method('render')->with('@ChillMain/Workflow/_signature_sign.html.twig', $this->isType('array'))
|
||||||
->willReturn('ok');
|
->willReturn('ok');
|
||||||
|
|
||||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
$controller = new WorkflowAddSignatureController($entityWorkflowManager, $pdfSignatureZoneAvailable, $normalizer, $twig);
|
||||||
|
|
||||||
$security = $this->createMock(Security::class);
|
|
||||||
$security->expects($this->once())->method('isGranted')->with(EntityWorkflowStepSignatureVoter::SIGN, $signature)
|
|
||||||
->willReturn(true);
|
|
||||||
|
|
||||||
$controller = new WorkflowAddSignatureController($entityWorkflowManager, $pdfSignatureZoneAvailable, $normalizer, $twig, $urlGenerator, $security);
|
|
||||||
|
|
||||||
$actual = $controller($signature, new Request());
|
$actual = $controller($signature, new Request());
|
||||||
|
|
||||||
|
@@ -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 Chill\MainBundle\Tests\Repository;
|
namespace Repository;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\NewsItem;
|
use Chill\MainBundle\Entity\NewsItem;
|
||||||
use Chill\MainBundle\Repository\NewsItemRepository;
|
use Chill\MainBundle\Repository\NewsItemRepository;
|
||||||
|
@@ -1,95 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Tests\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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,55 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace 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'));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,178 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Tests\Workflow\EventSubscriber;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowTransitionVoter;
|
|
||||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
|
||||||
use Chill\MainBundle\Workflow\EventSubscriber\EntityWorkflowGuardTransition;
|
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Prophecy\Argument;
|
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Workflow\DefinitionBuilder;
|
|
||||||
use Symfony\Component\Workflow\Exception\NotEnabledTransitionException;
|
|
||||||
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
|
||||||
use Symfony\Component\Workflow\Registry;
|
|
||||||
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
|
||||||
use Symfony\Component\Workflow\Transition;
|
|
||||||
use Symfony\Component\Workflow\Workflow;
|
|
||||||
use Symfony\Component\Workflow\WorkflowInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class EntityWorkflowGuardTransitionTest extends TestCase
|
|
||||||
{
|
|
||||||
use ProphecyTrait;
|
|
||||||
|
|
||||||
public static function buildRegistry(?EventSubscriberInterface $eventSubscriber): Registry
|
|
||||||
{
|
|
||||||
$builder = new DefinitionBuilder();
|
|
||||||
$builder
|
|
||||||
->setInitialPlaces(['initial'])
|
|
||||||
->addPlaces(['initial', 'intermediate', 'step1', 'step2', 'step3'])
|
|
||||||
->addTransition(new Transition('intermediate', 'initial', 'intermediate'))
|
|
||||||
->addTransition($transition1 = new Transition('transition1', 'intermediate', 'step1'))
|
|
||||||
->addTransition($transition2 = new Transition('transition2', 'intermediate', 'step2'))
|
|
||||||
->addTransition($transition3 = new Transition('transition3', 'intermediate', 'step3'))
|
|
||||||
;
|
|
||||||
|
|
||||||
$transitionMetadata = new \SplObjectStorage();
|
|
||||||
$transitionMetadata->attach($transition1, ['transitionGuard' => 'only-dest']);
|
|
||||||
$transitionMetadata->attach($transition2, ['transitionGuard' => 'only-dest+system']);
|
|
||||||
$transitionMetadata->attach($transition3, ['transitionGuard' => 'system']);
|
|
||||||
|
|
||||||
$builder->setMetadataStore(new InMemoryMetadataStore(transitionsMetadata: $transitionMetadata));
|
|
||||||
|
|
||||||
if (null !== $eventSubscriber) {
|
|
||||||
$eventDispatcher = new EventDispatcher();
|
|
||||||
$eventDispatcher->addSubscriber($eventSubscriber);
|
|
||||||
}
|
|
||||||
|
|
||||||
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), $eventDispatcher ?? null, 'dummy');
|
|
||||||
|
|
||||||
$registry = new Registry();
|
|
||||||
$registry->addWorkflow(
|
|
||||||
$workflow,
|
|
||||||
new class () implements WorkflowSupportStrategyInterface {
|
|
||||||
public function supports(WorkflowInterface $workflow, object $subject): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return $registry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider provideBlockingTransition
|
|
||||||
*/
|
|
||||||
public function testTransitionGuardBlocked(EntityWorkflow $entityWorkflow, string $transition, ?User $user, bool $isGrantedAllTransition, string $uuid): void
|
|
||||||
{
|
|
||||||
$userRender = $this->prophesize(UserRender::class);
|
|
||||||
$userRender->renderString(Argument::type(User::class), [])->willReturn('user-as-string');
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->getUser()->willReturn($user);
|
|
||||||
$security->isGranted(EntityWorkflowTransitionVoter::APPLY_ALL_TRANSITIONS, $entityWorkflow->getCurrentStep())
|
|
||||||
->willReturn($isGrantedAllTransition);
|
|
||||||
|
|
||||||
$transitionGuard = new EntityWorkflowGuardTransition($userRender->reveal(), $security->reveal());
|
|
||||||
$registry = self::buildRegistry($transitionGuard);
|
|
||||||
|
|
||||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
|
||||||
|
|
||||||
$context = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
|
|
||||||
self::expectException(NotEnabledTransitionException::class);
|
|
||||||
try {
|
|
||||||
$workflow->apply($entityWorkflow, $transition, ['context' => $context, 'byUser' => $user, 'transition' => $transition, 'transitionAt' => new \DateTimeImmutable('now')]);
|
|
||||||
} catch (NotEnabledTransitionException $e) {
|
|
||||||
$list = $e->getTransitionBlockerList();
|
|
||||||
|
|
||||||
self::assertEquals(1, $list->count());
|
|
||||||
$list = iterator_to_array($list->getIterator());
|
|
||||||
self::assertEquals($uuid, $list[0]->getCode());
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider provideValidTransition
|
|
||||||
*/
|
|
||||||
public function testTransitionGuardValid(EntityWorkflow $entityWorkflow, string $transition, ?User $user, bool $isGrantedAllTransition, string $newStep): void
|
|
||||||
{
|
|
||||||
$userRender = $this->prophesize(UserRender::class);
|
|
||||||
$userRender->renderString(Argument::type(User::class), [])->willReturn('user-as-string');
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->getUser()->willReturn($user);
|
|
||||||
$security->isGranted(EntityWorkflowTransitionVoter::APPLY_ALL_TRANSITIONS, $entityWorkflow->getCurrentStep())
|
|
||||||
->willReturn($isGrantedAllTransition);
|
|
||||||
|
|
||||||
$transitionGuard = new EntityWorkflowGuardTransition($userRender->reveal(), $security->reveal());
|
|
||||||
$registry = self::buildRegistry($transitionGuard);
|
|
||||||
|
|
||||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
|
||||||
$context = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
|
|
||||||
$workflow->apply($entityWorkflow, $transition, ['context' => $context, 'byUser' => $user, 'transition' => $transition, 'transitionAt' => new \DateTimeImmutable('now')]);
|
|
||||||
|
|
||||||
self::assertEquals($newStep, $entityWorkflow->getStep());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function provideBlockingTransition(): iterable
|
|
||||||
{
|
|
||||||
yield [self::buildEntityWorkflow([new User()]), 'transition1', new User(), false, 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc'];
|
|
||||||
yield [self::buildEntityWorkflow([]), 'transition1', null, false, 'd9e39a18-704c-11ef-b235-8fe0619caee7'];
|
|
||||||
yield [self::buildEntityWorkflow([new User()]), 'transition1', null, false, 'd9e39a18-704c-11ef-b235-8fe0619caee7'];
|
|
||||||
yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, false, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb'];
|
|
||||||
yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, true, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function provideValidTransition(): iterable
|
|
||||||
{
|
|
||||||
yield [self::buildEntityWorkflow([$u = new User()]), 'transition1', $u, false, 'step1'];
|
|
||||||
yield [self::buildEntityWorkflow([$u = new User()]), 'transition2', $u, false, 'step2'];
|
|
||||||
yield [self::buildEntityWorkflow([new User()]), 'transition2', null, false, 'step2'];
|
|
||||||
yield [self::buildEntityWorkflow([]), 'transition2', null, false, 'step2'];
|
|
||||||
yield [self::buildEntityWorkflow([new User()]), 'transition3', null, false, 'step3'];
|
|
||||||
yield [self::buildEntityWorkflow([]), 'transition3', null, false, 'step3'];
|
|
||||||
|
|
||||||
// transition allowed thanks to permission "apply all transitions"
|
|
||||||
yield [self::buildEntityWorkflow([new User()]), 'transition1', new User(), true, 'step1'];
|
|
||||||
yield [self::buildEntityWorkflow([new User()]), 'transition2', new User(), true, 'step2'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function buildEntityWorkflow(array $futureDestUsers): EntityWorkflow
|
|
||||||
{
|
|
||||||
$registry = self::buildRegistry(null);
|
|
||||||
$baseContext = ['transition' => 'intermediate', 'transitionAt' => new \DateTimeImmutable()];
|
|
||||||
|
|
||||||
// test a user not is destination is blocked
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers = $futureDestUsers;
|
|
||||||
$workflow->apply($entityWorkflow, 'intermediate', ['context' => $dto, ...$baseContext]);
|
|
||||||
|
|
||||||
return $entityWorkflow;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,146 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Tests\Workflow;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
|
||||||
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
|
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
|
||||||
use Chill\PersonBundle\Entity\Person;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
use Symfony\Component\Clock\MockClock;
|
|
||||||
use Symfony\Component\Workflow\DefinitionBuilder;
|
|
||||||
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
|
||||||
use Symfony\Component\Workflow\Registry;
|
|
||||||
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
|
||||||
use Symfony\Component\Workflow\Transition;
|
|
||||||
use Symfony\Component\Workflow\Workflow;
|
|
||||||
use Symfony\Component\Workflow\WorkflowInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class SignatureStepStateChangerTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testMarkSignatureAsSignedScenarioWhichExpectsTransition()
|
|
||||||
{
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$entityWorkflow->setWorkflowName('dummy');
|
|
||||||
$registry = $this->buildRegistry();
|
|
||||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
|
||||||
$clock = new MockClock();
|
|
||||||
$user = new User();
|
|
||||||
$changer = new SignatureStepStateChanger($registry, $clock, new NullLogger());
|
|
||||||
|
|
||||||
// move it to signature
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futurePersonSignatures = [new Person(), new Person()];
|
|
||||||
$workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transitionAt' => $clock->now(),
|
|
||||||
'byUser' => $user, 'transition' => 'to_signature']);
|
|
||||||
|
|
||||||
// get the signature created
|
|
||||||
$signatures = $entityWorkflow->getCurrentStep()->getSignatures();
|
|
||||||
|
|
||||||
if (2 !== count($signatures)) {
|
|
||||||
throw new \LogicException('there should have 2 signatures at this step');
|
|
||||||
}
|
|
||||||
|
|
||||||
// we mark the first signature as signed
|
|
||||||
$changer->markSignatureAsSigned($signatures[0], 1);
|
|
||||||
|
|
||||||
self::assertEquals('signature', $entityWorkflow->getStep(), 'there should have any change in the entity workflow step');
|
|
||||||
self::assertEquals(EntityWorkflowSignatureStateEnum::SIGNED, $signatures[0]->getState());
|
|
||||||
self::assertEquals(1, $signatures[0]->getZoneSignatureIndex());
|
|
||||||
self::assertNotNull($signatures[0]->getStateDate());
|
|
||||||
|
|
||||||
|
|
||||||
// we mark the second signature as signed
|
|
||||||
$changer->markSignatureAsSigned($signatures[1], 2);
|
|
||||||
self::assertEquals(EntityWorkflowSignatureStateEnum::SIGNED, $signatures[1]->getState());
|
|
||||||
self::assertEquals('post-signature', $entityWorkflow->getStep(), 'the entity workflow step should be post-signature');
|
|
||||||
self::assertContains($user, $entityWorkflow->getCurrentStep()->getAllDestUser());
|
|
||||||
self::assertEquals(2, $signatures[1]->getZoneSignatureIndex());
|
|
||||||
self::assertNotNull($signatures[1]->getStateDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testMarkSignatureAsSignedScenarioWithoutRequiredMetadata()
|
|
||||||
{
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$entityWorkflow->setWorkflowName('dummy');
|
|
||||||
$registry = $this->buildRegistry();
|
|
||||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
|
||||||
$clock = new MockClock();
|
|
||||||
$user = new User();
|
|
||||||
$changer = new SignatureStepStateChanger($registry, $clock, new NullLogger());
|
|
||||||
|
|
||||||
// move it to signature
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futurePersonSignatures = [new Person()];
|
|
||||||
$workflow->apply($entityWorkflow, 'to_signature-without-metadata', ['context' => $dto, 'transitionAt' => $clock->now(),
|
|
||||||
'byUser' => $user, 'transition' => 'to_signature-without-metadata']);
|
|
||||||
|
|
||||||
// get the signature created
|
|
||||||
$signatures = $entityWorkflow->getCurrentStep()->getSignatures();
|
|
||||||
|
|
||||||
if (1 !== count($signatures)) {
|
|
||||||
throw new \LogicException('there should have 2 signatures at this step');
|
|
||||||
}
|
|
||||||
|
|
||||||
// we mark the first signature as signed
|
|
||||||
$changer->markSignatureAsSigned($signatures[0], 1);
|
|
||||||
|
|
||||||
self::assertEquals('signature-without-metadata', $entityWorkflow->getStep(), 'there should have any change in the entity workflow step');
|
|
||||||
self::assertEquals(EntityWorkflowSignatureStateEnum::SIGNED, $signatures[0]->getState());
|
|
||||||
self::assertEquals(1, $signatures[0]->getZoneSignatureIndex());
|
|
||||||
self::assertNotNull($signatures[0]->getStateDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildRegistry(): Registry
|
|
||||||
{
|
|
||||||
$builder = new DefinitionBuilder();
|
|
||||||
$builder
|
|
||||||
->setInitialPlaces('initial')
|
|
||||||
->addPlaces(['initial', 'signature', 'signature-without-metadata', 'post-signature'])
|
|
||||||
->addTransition(new Transition('to_signature', 'initial', 'signature'))
|
|
||||||
->addTransition(new Transition('to_signature-without-metadata', 'initial', 'signature-without-metadata'))
|
|
||||||
->addTransition(new Transition('to_post-signature', 'signature', 'post-signature'))
|
|
||||||
->addTransition(new Transition('to_post-signature_2', 'signature-without-metadata', 'post-signature'))
|
|
||||||
;
|
|
||||||
|
|
||||||
$metadata = new InMemoryMetadataStore(
|
|
||||||
[],
|
|
||||||
[
|
|
||||||
'signature' => ['onSignatureCompleted' => ['transitionName' => 'to_post-signature']],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
$builder->setMetadataStore($metadata);
|
|
||||||
|
|
||||||
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
|
|
||||||
$registry = new Registry();
|
|
||||||
$registry->addWorkflow(
|
|
||||||
$workflow,
|
|
||||||
new class () implements WorkflowSupportStrategyInterface {
|
|
||||||
public function supports(WorkflowInterface $workflow, object $subject): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return $registry;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,123 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Workflow\EventSubscriber;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowTransitionVoter;
|
|
||||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
|
||||||
use Symfony\Component\Workflow\TransitionBlocker;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevent apply a transition on an entity workflow.
|
|
||||||
*
|
|
||||||
* This apply logic and rules to decide if a transition can be applyed.
|
|
||||||
*
|
|
||||||
* Those rules are:
|
|
||||||
*
|
|
||||||
* - if the transition is system-only or is allowed for user;
|
|
||||||
* - if the user is present in the dest users for a workflow;
|
|
||||||
* - or if the user have permission to apply all the transitions
|
|
||||||
*/
|
|
||||||
class EntityWorkflowGuardTransition implements EventSubscriberInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly UserRender $userRender,
|
|
||||||
private readonly Security $security,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'workflow.guard' => [
|
|
||||||
['guardEntityWorkflow', 0],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function guardEntityWorkflow(GuardEvent $event)
|
|
||||||
{
|
|
||||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var EntityWorkflow $entityWorkflow */
|
|
||||||
$entityWorkflow = $event->getSubject();
|
|
||||||
|
|
||||||
if ($entityWorkflow->isFinal()) {
|
|
||||||
$event->addTransitionBlocker(
|
|
||||||
new TransitionBlocker(
|
|
||||||
'workflow.The workflow is finalized',
|
|
||||||
'd6306280-7535-11ec-a40d-1f7bee26e2c0'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $this->security->getUser();
|
|
||||||
$metadata = $event->getWorkflow()->getMetadataStore()->getTransitionMetadata($event->getTransition());
|
|
||||||
$systemTransitions = explode('+', $metadata['transitionGuard'] ?? 'only-dest');
|
|
||||||
|
|
||||||
if (null === $user) {
|
|
||||||
if (in_array('system', $systemTransitions, true)) {
|
|
||||||
// it is safe to apply this transition
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$event->addTransitionBlocker(
|
|
||||||
new TransitionBlocker(
|
|
||||||
'workflow.Transition is not allowed for system',
|
|
||||||
'd9e39a18-704c-11ef-b235-8fe0619caee7'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// for users
|
|
||||||
if (!in_array('only-dest', $systemTransitions, true)) {
|
|
||||||
$event->addTransitionBlocker(
|
|
||||||
new TransitionBlocker(
|
|
||||||
'workflow.Only system can apply this transition',
|
|
||||||
'5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($user)
|
|
||||||
) {
|
|
||||||
if ($event->getMarking()->has('initial')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->security->isGranted(EntityWorkflowTransitionVoter::APPLY_ALL_TRANSITIONS, $entityWorkflow->getCurrentStep())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$event->addTransitionBlocker(new TransitionBlocker(
|
|
||||||
'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%',
|
|
||||||
'f3eeb57c-7532-11ec-9495-e7942a2ac7bc',
|
|
||||||
[
|
|
||||||
'%users%' => implode(
|
|
||||||
', ',
|
|
||||||
$entityWorkflow->getCurrentStep()->getAllDestUser()->map(fn (User $u) => $this->userRender->renderString($u, []))->toArray()
|
|
||||||
),
|
|
||||||
]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -13,16 +13,20 @@ namespace Chill\MainBundle\Workflow\EventSubscriber;
|
|||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Workflow\Event\Event;
|
use Symfony\Component\Workflow\Event\Event;
|
||||||
|
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||||
|
use Symfony\Component\Workflow\TransitionBlocker;
|
||||||
|
|
||||||
final readonly class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
|
final readonly class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private LoggerInterface $chillLogger,
|
private LoggerInterface $chillLogger,
|
||||||
private Security $security,
|
private Security $security,
|
||||||
|
private UserRender $userRender,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
public static function getSubscribedEvents(): array
|
||||||
@@ -32,9 +36,48 @@ final readonly class EntityWorkflowTransitionEventSubscriber implements EventSub
|
|||||||
'workflow.completed' => [
|
'workflow.completed' => [
|
||||||
['markAsFinal', 2048],
|
['markAsFinal', 2048],
|
||||||
],
|
],
|
||||||
|
'workflow.guard' => [
|
||||||
|
['guardEntityWorkflow', 0],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function guardEntityWorkflow(GuardEvent $event)
|
||||||
|
{
|
||||||
|
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var EntityWorkflow $entityWorkflow */
|
||||||
|
$entityWorkflow = $event->getSubject();
|
||||||
|
|
||||||
|
if ($entityWorkflow->isFinal()) {
|
||||||
|
$event->addTransitionBlocker(
|
||||||
|
new TransitionBlocker(
|
||||||
|
'workflow.The workflow is finalized',
|
||||||
|
'd6306280-7535-11ec-a40d-1f7bee26e2c0'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($this->security->getUser())) {
|
||||||
|
if (!$event->getMarking()->has('initial')) {
|
||||||
|
$event->addTransitionBlocker(new TransitionBlocker(
|
||||||
|
'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%',
|
||||||
|
'f3eeb57c-7532-11ec-9495-e7942a2ac7bc',
|
||||||
|
[
|
||||||
|
'%users%' => implode(
|
||||||
|
', ',
|
||||||
|
$entityWorkflow->getCurrentStep()->getAllDestUser()->map(fn (User $u) => $this->userRender->renderString($u, []))->toArray()
|
||||||
|
),
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function markAsFinal(Event $event): void
|
public function markAsFinal(Event $event): void
|
||||||
{
|
{
|
||||||
// NOTE: it is not possible to move this method to the marking store, because
|
// NOTE: it is not possible to move this method to the marking store, because
|
||||||
@@ -66,13 +109,11 @@ final readonly class EntityWorkflowTransitionEventSubscriber implements EventSub
|
|||||||
/** @var EntityWorkflow $entityWorkflow */
|
/** @var EntityWorkflow $entityWorkflow */
|
||||||
$entityWorkflow = $event->getSubject();
|
$entityWorkflow = $event->getSubject();
|
||||||
|
|
||||||
$user = $this->security->getUser();
|
|
||||||
|
|
||||||
$this->chillLogger->info('[workflow] apply transition on entityWorkflow', [
|
$this->chillLogger->info('[workflow] apply transition on entityWorkflow', [
|
||||||
'relatedEntityClass' => $entityWorkflow->getRelatedEntityClass(),
|
'relatedEntityClass' => $entityWorkflow->getRelatedEntityClass(),
|
||||||
'relatedEntityId' => $entityWorkflow->getRelatedEntityId(),
|
'relatedEntityId' => $entityWorkflow->getRelatedEntityId(),
|
||||||
'transition' => $event->getTransition()->getName(),
|
'transition' => $event->getTransition()->getName(),
|
||||||
'by_user' => $user instanceof User ? $user->getId() : (string) $user?->getUserIdentifier(),
|
'by_user' => $this->security->getUser(),
|
||||||
'entityWorkflow' => $entityWorkflow->getId(),
|
'entityWorkflow' => $entityWorkflow->getId(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@@ -1,112 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Workflow;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Component\Clock\ClockInterface;
|
|
||||||
use Symfony\Component\Workflow\Registry;
|
|
||||||
|
|
||||||
class SignatureStepStateChanger
|
|
||||||
{
|
|
||||||
private const LOG_PREFIX = '[SignatureStepStateChanger] ';
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly Registry $registry,
|
|
||||||
private readonly ClockInterface $clock,
|
|
||||||
private readonly LoggerInterface $logger,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): void
|
|
||||||
{
|
|
||||||
$signature
|
|
||||||
->setState(EntityWorkflowSignatureStateEnum::SIGNED)
|
|
||||||
->setZoneSignatureIndex($atIndex)
|
|
||||||
->setStateDate($this->clock->now())
|
|
||||||
;
|
|
||||||
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as signed', ['signatureId' => $signature->getId(), 'index' => (string) $atIndex]);
|
|
||||||
|
|
||||||
if (!EntityWorkflowStepSignature::isAllSignatureNotPendingForStep($signature->getStep())) {
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'This is not the last signature, skipping transition to another place', ['signatureId' => $signature->getId()]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logger->debug(self::LOG_PREFIX.'Continuing the process to find a transition', ['signatureId' => $signature->getId()]);
|
|
||||||
|
|
||||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
|
||||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
|
||||||
$metadataStore = $workflow->getMetadataStore();
|
|
||||||
|
|
||||||
// find a transition
|
|
||||||
$marking = $workflow->getMarking($entityWorkflow);
|
|
||||||
$places = $marking->getPlaces();
|
|
||||||
|
|
||||||
$transition = null;
|
|
||||||
foreach ($places as $place => $int) {
|
|
||||||
$metadata = $metadataStore->getPlaceMetadata($place);
|
|
||||||
if (array_key_exists('onSignatureCompleted', $metadata)) {
|
|
||||||
$transition = $metadata['onSignatureCompleted']['transitionName'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $transition) {
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'The transition is not configured, will not apply a transition', ['signatureId' => $signature->getId()]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$previousUser = $this->getPreviousSender($signature->getStep());
|
|
||||||
|
|
||||||
if (null === $previousUser) {
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'No previous user, will not apply a transition', ['signatureId' => $signature->getId()]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$transitionDto->futureDestUsers[] = $previousUser;
|
|
||||||
|
|
||||||
$workflow->apply($entityWorkflow, $transition, [
|
|
||||||
'context' => $transitionDto,
|
|
||||||
'transitionAt' => $this->clock->now(),
|
|
||||||
'transition' => $transition,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User
|
|
||||||
{
|
|
||||||
$stepsChained = $entityWorkflowStep->getEntityWorkflow()->getStepsChained();
|
|
||||||
|
|
||||||
foreach ($stepsChained as $stepChained) {
|
|
||||||
if ($stepChained === $entityWorkflowStep) {
|
|
||||||
if (null === $previous = $stepChained->getPrevious()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $previousUser = $previous->getTransitionBy()) {
|
|
||||||
return $previousUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->getPreviousSender($previous);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \LogicException('no same step found');
|
|
||||||
}
|
|
||||||
}
|
|
@@ -165,6 +165,7 @@ components:
|
|||||||
endDate:
|
endDate:
|
||||||
$ref: "#/components/schemas/Date"
|
$ref: "#/components/schemas/Date"
|
||||||
|
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/1.0/search.json:
|
/1.0/search.json:
|
||||||
get:
|
get:
|
||||||
@@ -236,7 +237,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:
|
||||||
@@ -274,7 +275,7 @@ paths:
|
|||||||
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:
|
||||||
@@ -320,7 +321,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:
|
||||||
@@ -343,6 +344,7 @@ 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:
|
||||||
@@ -363,7 +365,7 @@ paths:
|
|||||||
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:
|
||||||
@@ -404,7 +406,7 @@ paths:
|
|||||||
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:
|
||||||
@@ -437,7 +439,7 @@ paths:
|
|||||||
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:
|
||||||
@@ -475,7 +477,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"
|
||||||
@@ -508,7 +510,7 @@ paths:
|
|||||||
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:
|
||||||
@@ -539,7 +541,7 @@ paths:
|
|||||||
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:
|
||||||
@@ -573,12 +575,13 @@ paths:
|
|||||||
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:
|
||||||
@@ -623,7 +626,7 @@ paths:
|
|||||||
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:
|
||||||
@@ -781,7 +784,7 @@ 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:
|
||||||
@@ -820,38 +823,6 @@ paths:
|
|||||||
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:
|
||||||
@@ -873,7 +844,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:
|
||||||
@@ -887,7 +858,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:
|
||||||
@@ -903,7 +874,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:
|
||||||
@@ -917,7 +888,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/DashboardConfigItem"
|
$ref: '#/components/schemas/DashboardConfigItem'
|
||||||
403:
|
403:
|
||||||
description: "Unauthorized"
|
description: "Unauthorized"
|
||||||
|
|
||||||
@@ -934,6 +905,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/NewsItem"
|
$ref: '#/components/schemas/NewsItem'
|
||||||
403:
|
403:
|
||||||
description: "Unauthorized"
|
description: "Unauthorized"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user