mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-20 05:35:00 +00:00
Compare commits
192 Commits
#361-impro
...
ticket/rea
Author | SHA1 | Date | |
---|---|---|---|
3aea3f502b
|
|||
def75cec6c
|
|||
41896b1dd4
|
|||
c5e6122d2c
|
|||
088b876e20 | |||
3400656d7c | |||
568c8be7fd
|
|||
538ecc42ea
|
|||
15d26d4b06
|
|||
d8bd9bd7cd
|
|||
dcdfba5ccd
|
|||
0204bdd38d
|
|||
392fd01b56
|
|||
35844f3b73 | |||
7506b918d7 | |||
cfba291f2c | |||
|
04438c09d3 | ||
2a54d1b909 | |||
|
628eeac5e0 | ||
a2263b3fa1 | |||
74796d0fb0 | |||
c19481e40a
|
|||
|
6eeb717b1a | ||
beb7c462da
|
|||
|
dbf363a9e8 | ||
64a2f7c9ed
|
|||
f26d9739c8 | |||
afa5edc1d8
|
|||
42d6c9e672
|
|||
2b22d4cb7c
|
|||
c8e5d0eb37
|
|||
2bf8ad5d6c | |||
11698a52e3 | |||
70955573e8 | |||
|
3df4043eb9 | ||
06e8264dde
|
|||
b451d2c4a3 | |||
4f93150874 | |||
0566ab0910 | |||
|
f4eeee1598 | ||
33cf16fc13 | |||
0a331aab37 | |||
d43b739654 | |||
c72432efae | |||
95975fae55
|
|||
95a7efa138
|
|||
45e193ff6d
|
|||
dfc146ff3f
|
|||
b41fcf66a9 | |||
|
a8dd1b3548 | ||
2b99a480ac
|
|||
7633e587bb
|
|||
fc61dfdf3a
|
|||
f1a5b5c49e
|
|||
ec685dcd47
|
|||
631ae3eedd
|
|||
440a7837ac
|
|||
e0abf34784
|
|||
377ae9a9dc
|
|||
034dc30e30
|
|||
d615111a0f
|
|||
ffb756c712
|
|||
69daccb860
|
|||
16435423cf
|
|||
697b4ab436
|
|||
67d804e28e
|
|||
cf41fa9574
|
|||
b8b325f7d7
|
|||
e97bd8c4ef
|
|||
e28d7df533
|
|||
4b20b1bc01
|
|||
b15733076c
|
|||
25be5c9ea3
|
|||
b035020c6f
|
|||
128101dc46
|
|||
5f2711023e
|
|||
bdf2ed4bbd
|
|||
1df542603e
|
|||
80bcc68ce5
|
|||
154fc3e2f6
|
|||
e45af94c78
|
|||
166a6fde20
|
|||
631f047338
|
|||
a777588bb8
|
|||
ca78d112c2
|
|||
bcfd317d83
|
|||
348740f073
|
|||
0d74f0980f
|
|||
be19dc00db
|
|||
643028ffd6
|
|||
ac4e2e5bf2
|
|||
498572b96e
|
|||
d2a61ce69b
|
|||
a9c0567ee1
|
|||
76cec5b5a8
|
|||
efe8a67697
|
|||
26dfa9b028
|
|||
50025044d3
|
|||
e6202a2e34
|
|||
b863bd967d
|
|||
e65bcf7275
|
|||
e00ece4200
|
|||
640fd71402
|
|||
aae50ca290
|
|||
1fa483598b
|
|||
e4b6a468f8
|
|||
|
66c7758023 | ||
|
4750d2c24e | ||
|
ca05e3d979 | ||
|
a20f9b4f86 | ||
|
c73c1eb8d5 | ||
|
8778bb0731 | ||
|
c7d20eebc5 | ||
|
b9e130c159 | ||
|
3e8bc94af3 | ||
|
0c914c9f9f | ||
|
580a60c939 | ||
|
4996ac3b7c | ||
|
2a23bf19cb | ||
|
650d2596d9 | ||
|
2bdd5a329e | ||
78d1776733
|
|||
66dc603c85
|
|||
3a8154ecce
|
|||
c81828e04f
|
|||
|
ec17dd7de2 | ||
76c076a5f3 | |||
|
f0045edd6c | ||
|
d00b76ffcd | ||
|
8991f0ef3f | ||
|
d6f5eae0c9 | ||
|
821fce3dd8 | ||
|
1d33ae1e39 | ||
|
19af0feb57 | ||
|
1c09e9a692 | ||
|
d72e748388 | ||
|
ab850b7b70 | ||
|
3f9745d8cf | ||
|
473765366a | ||
|
6500c24a7f | ||
|
1d00457141 | ||
|
eb0bf56cff | ||
|
7b8cd90cf1 | ||
|
a27d92aba0 | ||
|
85bdfb9e21 | ||
|
4cffcf4de1 | ||
|
b2587a688f | ||
|
c9f0e9843b | ||
|
b40ad9e445 | ||
|
3e10e47e29 | ||
|
2a1963e993 | ||
34c171659b | |||
2d8b960d9e
|
|||
831ae03431 | |||
45828174d1
|
|||
ed45f14a45
|
|||
fa67835690
|
|||
b434d38091
|
|||
|
800a952532 | ||
9f355032a8
|
|||
0bc6e62d4d
|
|||
46fb1c04b5
|
|||
3b2c3d1464 | |||
|
0bd6038160 | ||
|
baab8e94ce | ||
e2deb55fdb
|
|||
|
2cdfb50058 | ||
39d701feb2
|
|||
613ee8b186
|
|||
56a1a488de
|
|||
3f789ad0f4 | |||
467bea7cde
|
|||
670b8eb82b
|
|||
a9760b323f
|
|||
71a3a1924a
|
|||
ecdc1e25bf
|
|||
dd37427be1
|
|||
c8467df1b1
|
|||
4c89a954fa
|
|||
7c1f3b114d
|
|||
36bc4dab24
|
|||
4b30d92282
|
|||
75fbec5489
|
|||
912fdd6349
|
|||
5832542978
|
|||
5c3585a1ed
|
|||
a2f1e20ddf
|
|||
4d67702a76
|
|||
18e442db29 | |||
|
deb3d92189 | ||
a59ea7db31
|
|||
a738b0cac9
|
6
.changes/unreleased/Feature-20240530-160003.yaml
Normal file
6
.changes/unreleased/Feature-20240530-160003.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Feature
|
||||
body: |
|
||||
Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||
time: 2024-05-30T16:00:03.440767606+02:00
|
||||
custom:
|
||||
Issue: ""
|
6
.changes/unreleased/Feature-20240531-190242.yaml
Normal file
6
.changes/unreleased/Feature-20240531-190242.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Feature
|
||||
body: |
|
||||
Upgrade CKEditor and refactor configuration with use of typescript
|
||||
time: 2024-05-31T19:02:42.776662753+02:00
|
||||
custom:
|
||||
Issue: ""
|
@@ -1,4 +0,0 @@
|
||||
## v4.0.2 - 2025-07-09
|
||||
### Fixed
|
||||
* Fix add missing translation
|
||||
* Fix the transfer of evaluations and documents during of accompanyingperiodwork
|
@@ -19,11 +19,11 @@ max_line_length = 80
|
||||
[COMMIT_EDITMSG]
|
||||
max_line_length = 0
|
||||
|
||||
[*.{js, vue, ts}]
|
||||
[*.{js,vue,ts}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[.rst]
|
||||
ident_size = 3
|
||||
ident_style = space
|
||||
[*.rst]
|
||||
indent_size = 3
|
||||
indent_style = space
|
||||
|
||||
|
13
.env
13
.env
@@ -16,9 +16,6 @@ APP_ENV=prod
|
||||
APP_SECRET=!ChangeMeInAppEnv!
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
## Wopi server for editing documents online
|
||||
EDITOR_SERVER=http://collabora:9980
|
||||
|
||||
# must be manually set in .env.local
|
||||
# ADMIN_PASSWORD=
|
||||
|
||||
@@ -92,3 +89,13 @@ REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
|
||||
###> symfony/ovh-cloud-notifier ###
|
||||
# OVHCLOUD_DSN=ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME
|
||||
###< symfony/ovh-cloud-notifier ###
|
||||
|
||||
###> symfony/mercure-bundle ###
|
||||
# See https://symfony.com/doc/current/mercure.html#configuration
|
||||
# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
|
||||
MERCURE_URL=https://example.com/.well-known/mercure
|
||||
# The public URL of the Mercure hub, used by the browser to connect
|
||||
MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure
|
||||
# The secret used to sign the JWTs
|
||||
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
|
||||
###< symfony/mercure-bundle ###
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -185,57 +185,14 @@ When we need to use a DateTime or DateTimeImmutable that need to express "now",
|
||||
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
|
||||
where injection does not work when restoring an entity from database, but usually possible in services.
|
||||
|
||||
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
|
||||
where we have full and easy control of the date.
|
||||
|
||||
### Testing Information
|
||||
|
||||
The project uses PHPUnit for testing. Each bundle has its own test suite, and there's also a global test suite at the root level.
|
||||
|
||||
#### Use of mock in tests
|
||||
|
||||
##### General mocking
|
||||
|
||||
For creating mock, we prefer using prophecy (library phpspec/prophecy).
|
||||
|
||||
##### Useful helpers and tips that avoid create a mock
|
||||
|
||||
Some notable implementations that are tests helper, and avoid to create a mock:
|
||||
|
||||
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
|
||||
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
|
||||
- `\Symfony\Component\HttpClient\MockHttpClient`, an implementation of `\Symfony\Contracts\HttpClient\HttpClientInterface`;
|
||||
- When using `\Symfony\Component\Mailer\MailerInterface`, we can create the mock with "InMemoryTransport":
|
||||
|
||||
```php
|
||||
use Symfony\Component\Mailer\Transport\InMemoryTransport;
|
||||
use \Symfony\Component\Mailer\Mailer;
|
||||
|
||||
$transport = new InMemoryTransport();
|
||||
$mailer = new Mailer($transport);
|
||||
|
||||
// After sending:
|
||||
$messages = $transport->getSent(); // array of SentMessage
|
||||
```
|
||||
- When using `\Symfony\Contracts\EventDispatcher\EventDispatcherInterface`, we can use directly an instance of `\Symfony\Component\EventDispatcher\EventDispatcher`;
|
||||
|
||||
##### When we prefer not creating a mock
|
||||
|
||||
- When we use Doctrine Entities related to the project, we prefer not to use a mock: we instantiate them directly (unless it requires too much code to write);
|
||||
|
||||
##### Mocking final and readonly classes
|
||||
|
||||
Classes marked as final can't be mocked. To avoid that, either:
|
||||
|
||||
- we remove the `final` keyword from the class;
|
||||
- we extract an interface from the final class.
|
||||
|
||||
This must be a decision made by a human, not by an AI. Every AI task must abort with an explicit message in that case.
|
||||
|
||||
#### Running Tests
|
||||
|
||||
The tests are run from the project's root (not from the bundle's root).
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
vendor/bin/phpunit
|
||||
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
30
.vscode/launch.json
vendored
Normal file
30
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Chill Debug",
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"port": 9000,
|
||||
"pathMappings": {
|
||||
"/var/www/html": "${workspaceFolder}"
|
||||
},
|
||||
"preLaunchTask": "symfony"
|
||||
},
|
||||
{
|
||||
"name": "Yarn Encore Dev (Watch)",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "yarn encore dev --watch",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Chill Debug + Yarn Encore Dev (Watch)",
|
||||
"configurations": ["Chill Debug", "Yarn Encore Dev (Watch)"]
|
||||
}
|
||||
]
|
||||
}
|
23
.vscode/tasks.json
vendored
Normal file
23
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "symfony",
|
||||
"args": [
|
||||
"server:start",
|
||||
"--allow-http",
|
||||
"--no-tls",
|
||||
"--port=8000",
|
||||
"--allow-all-ip",
|
||||
"-d"
|
||||
],
|
||||
"label": "symfony"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn",
|
||||
"args": ["encore", "dev", "--watch"],
|
||||
"label": "webpack"
|
||||
}
|
||||
]
|
||||
}
|
@@ -6,11 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v4.0.2 - 2025-07-09
|
||||
### Fixed
|
||||
* Fix add missing translation
|
||||
* Fix the transfer of evaluations and documents during of accompanyingperiodwork
|
||||
|
||||
## v4.0.1 - 2025-07-08
|
||||
### Fixed
|
||||
* Fix package.json for compilation
|
||||
|
@@ -54,7 +54,7 @@ Arborescence:
|
||||
- person
|
||||
- personvendee
|
||||
- household_edit_metadata
|
||||
- index.js
|
||||
- index.ts
|
||||
```
|
||||
|
||||
## Organisation des feuilles de styles
|
||||
|
@@ -32,3 +32,9 @@ services:
|
||||
hostname: my-rabbit
|
||||
volumes:
|
||||
- ./docker/rabbitmq/data:/var/lib/rabbitmq
|
||||
|
||||
###> symfony/mercure-bundle ###
|
||||
mercure:
|
||||
ports:
|
||||
- "127.0.0.1:8043:443"
|
||||
###< symfony/mercure-bundle ###
|
||||
|
31
compose.yaml
31
compose.yaml
@@ -50,7 +50,36 @@ services:
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
|
||||
###> symfony/mercure-bundle ###
|
||||
mercure:
|
||||
image: dunglas/mercure
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Uncomment the following line to disable HTTPS,
|
||||
#SERVER_NAME: ':80'
|
||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
|
||||
# Set the URL of your Symfony project (without trailing slash!) as value of the cors_origins directive
|
||||
MERCURE_EXTRA_DIRECTIVES: |
|
||||
cors_origins http://chill-bundles.wip https://chill-bundles.wip
|
||||
# Comment the following line to disable the development mode
|
||||
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "https://localhost/healthz" ]
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
volumes:
|
||||
- mercure_data:/data
|
||||
- mercure_config:/config
|
||||
###< symfony/mercure-bundle ###
|
||||
|
||||
volumes:
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database_data:
|
||||
###< doctrine/doctrine-bundle ###
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/mercure-bundle ###
|
||||
mercure_data:
|
||||
mercure_config:
|
||||
###< symfony/mercure-bundle ###
|
||||
|
@@ -55,6 +55,7 @@
|
||||
"symfony/http-foundation": "^5.4",
|
||||
"symfony/intl": "^5.4",
|
||||
"symfony/mailer": "^5.4",
|
||||
"symfony/mercure-bundle": "^0.3.9",
|
||||
"symfony/messenger": "^5.4",
|
||||
"symfony/mime": "^5.4",
|
||||
"symfony/monolog-bundle": "^3.5",
|
||||
@@ -133,6 +134,7 @@
|
||||
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
|
||||
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle",
|
||||
"Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src",
|
||||
"Chill\\TicketBundle\\": "src/Bundle/ChillTicketBundle/src",
|
||||
"Chill\\Utils\\Rector\\": "utils/rector/src"
|
||||
}
|
||||
},
|
||||
|
@@ -35,6 +35,8 @@ return [
|
||||
Chill\ThirdPartyBundle\ChillThirdPartyBundle::class => ['all' => true],
|
||||
Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true],
|
||||
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
|
||||
Chill\TicketBundle\ChillTicketBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
||||
];
|
||||
|
4
config/packages/chill_ticket.yaml
Normal file
4
config/packages/chill_ticket.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
chill_ticket:
|
||||
ticket:
|
||||
person_per_ticket: one # One of "one"; "many"
|
||||
|
@@ -14,6 +14,7 @@ doctrine_migrations:
|
||||
'Chill\Migrations\Calendar': '@ChillCalendarBundle/migrations'
|
||||
'Chill\Migrations\Budget': '@ChillBudgetBundle/migrations'
|
||||
'Chill\Migrations\Report': '@ChillReportBundle/migrations'
|
||||
'Chill\Migrations\Ticket': '@ChillTicketBundle/migrations'
|
||||
all_or_nothing:
|
||||
true
|
||||
|
||||
|
8
config/packages/mercure.yaml
Normal file
8
config/packages/mercure.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
mercure:
|
||||
hubs:
|
||||
default:
|
||||
url: '%env(MERCURE_URL)%'
|
||||
public_url: '%env(MERCURE_PUBLIC_URL)%'
|
||||
jwt:
|
||||
secret: '%env(MERCURE_JWT_SECRET)%'
|
||||
publish: '*'
|
@@ -62,10 +62,8 @@ framework:
|
||||
'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority
|
||||
'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async
|
||||
'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async
|
||||
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage': async
|
||||
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
|
||||
'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
|
||||
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async
|
||||
# end of routes added by chill-bundles recipes
|
||||
# Route your messages to the transports
|
||||
# 'App\Message\YourMessage': async
|
||||
|
@@ -17,9 +17,8 @@ when@dev:
|
||||
defaults:
|
||||
template: '@ChillMain/Dev/dev.assets.test2.html.twig'
|
||||
|
||||
|
||||
sass_address_picker:
|
||||
path: /_dev/address-picker
|
||||
dev_mercure:
|
||||
path: /_dev/mercure
|
||||
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
|
||||
defaults:
|
||||
template: '@ChillMain/Dev/dev.address-picker.html.twig'
|
||||
template: '@ChillMain/Dev/dev.mercure.html.twig'
|
||||
|
2
config/routes/chill_ticket.yaml
Normal file
2
config/routes/chill_ticket.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
chill_ticket_bundle:
|
||||
resource: '@ChillTicketBundle/config/routes.yaml'
|
@@ -11,24 +11,94 @@
|
||||
Create a new bundle
|
||||
*******************
|
||||
|
||||
Create your own bundle is not a trivial task.
|
||||
|
||||
The easiest way to achieve this is seems to be :
|
||||
|
||||
1. Prepare a fresh installation of the chill project, in a new directory
|
||||
2. Create a new bundle in this project, in the src directory
|
||||
3. Initialize a git repository **at the root bundle**, and create your initial commit.
|
||||
4. Register the bundle with composer/packagist. If you do not plan to distribute your bundle with packagist, you may use a custom repository for achieve this [#f1]_
|
||||
5. Move to a development installation, made as described in the :ref:`installation-for-development` section, and add your new repository to the composer.json file
|
||||
6. Work as :ref:`usual <editing-code-and-commiting>`
|
||||
|
||||
.. warning::
|
||||
|
||||
This part of the doc is not yet tested
|
||||
|
||||
TODO
|
||||
Create a new directory with Bundle class
|
||||
----------------------------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
mkdir -p src/Bundle/ChillSomeBundle/src/config
|
||||
mkdir -p src/Bundle/ChillSomeBundle/src/Controller
|
||||
|
||||
Add a bundle file
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?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\SomeBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class ChillSomeBundle extends Bundle {}
|
||||
|
||||
And a route file:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
chill_ticket_controller:
|
||||
resource: '@ChillTicketBundle/Controller/'
|
||||
type: annotation
|
||||
|
||||
Register the new psr-4 namespace
|
||||
--------------------------------
|
||||
|
||||
In composer.json, add the new psr4 namespace
|
||||
|
||||
.. code-block:: diff
|
||||
|
||||
{
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
+ "Chill\\SomeBundle\\": "src/Bundle/ChillSomeBundle/src",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.. rubric:: Footnotes
|
||||
Register the bundle
|
||||
-------------------
|
||||
|
||||
Register in the file :code:`config/bundles.php`:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
Vendor\Bundle\YourBundle\YourBundle::class => ['all' => true],
|
||||
|
||||
And import routes in :code:`config/routes/chill_some_bundle.yaml`:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
chill_ticket_bundle:
|
||||
resource: '@ChillSomeBundle/config/routes.yaml'
|
||||
|
||||
Add the doctrine_migration namespace
|
||||
------------------------------------
|
||||
|
||||
Add the namespace to :code:`config/packages/doctrine_migrations_chill.yaml`
|
||||
|
||||
.. code-block:: diff
|
||||
|
||||
doctrine_migrations:
|
||||
migrations_paths:
|
||||
+ 'Chill\Some\Ticket': '@ChillSomeBundle/migrations'
|
||||
|
||||
Dump autoloading
|
||||
----------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
symfony composer dump-autoload
|
||||
|
||||
.. [#f1] Be aware that we use the Affero GPL Licence, which ensure that all users must have access to derivative works done with this software.
|
||||
|
@@ -11,6 +11,7 @@
|
||||
"@hotwired/stimulus": "^3.0.0",
|
||||
"@luminateone/eslint-baseline": "^1.0.9",
|
||||
"@symfony/stimulus-bridge": "^3.2.0",
|
||||
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
|
||||
"@symfony/webpack-encore": "^4.1.0",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
@@ -45,7 +46,6 @@
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fragaria/address-formatter": "^6.6.1",
|
||||
"@fullcalendar/core": "^6.1.4",
|
||||
"@fullcalendar/daygrid": "^6.1.4",
|
||||
"@fullcalendar/interaction": "^6.1.4",
|
||||
@@ -70,7 +70,6 @@
|
||||
"vue-i18n": "^9.1.6",
|
||||
"vue-multiselect": "3.0.0-alpha.2",
|
||||
"vue-toast-notification": "^3.1.2",
|
||||
"vue-use-leaflet": "^0.1.7",
|
||||
"vuex": "^4.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
@@ -81,12 +80,12 @@
|
||||
"dev": "encore dev",
|
||||
"watch": "encore dev --watch",
|
||||
"build": "encore production --progress",
|
||||
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml> templates/api/specs.yaml",
|
||||
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml src/Bundle/ChillTicketBundle/chill.api.specs.yaml> templates/api/specs.yaml",
|
||||
"specs-validate": "swagger-cli validate templates/api/specs.yaml",
|
||||
"specs-create-dir": "mkdir -p templates/api",
|
||||
"specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
|
||||
"version": "node --version",
|
||||
"eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
|
||||
"eslint": "eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
@@ -58,6 +58,10 @@
|
||||
<!-- temporarily removed, the time to find a fix -->
|
||||
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude>
|
||||
</testsuite>
|
||||
|
||||
<testsuite name="TicketBundle">
|
||||
<directory suffix="Test.php">src/Bundle/ChillTicketBundle/tests/</directory>
|
||||
</testsuite>
|
||||
<!--
|
||||
<testsuite name="ReportBundle">
|
||||
<directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory>
|
||||
|
20
resources/dev.Caddyfile
Normal file
20
resources/dev.Caddyfile
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
# Désactive les redirections automatiques HTTP -> HTTPS
|
||||
# auto_https off
|
||||
# Désactive le port 80 par défaut
|
||||
# default_bind :8080
|
||||
}
|
||||
|
||||
localhost:8043 {
|
||||
mercure {
|
||||
# Publisher JWT key
|
||||
publisher_jwt !ChangeThisMercureHubJWTSecretKey!
|
||||
# Subscriber JWT key
|
||||
subscriber_jwt !ChangeThisMercureHubJWTSecretKey!
|
||||
cors_origins http://chill-bundles.wip https://chill-bundles.wip
|
||||
ui
|
||||
demo
|
||||
}
|
||||
|
||||
respond "Not Found" 404
|
||||
}
|
@@ -10,10 +10,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
getContext === 'accompanyingCourse' &&
|
||||
suggestedEntities.length > 0
|
||||
"
|
||||
v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0"
|
||||
>
|
||||
<ul class="list-suggest add-items inline">
|
||||
<li
|
||||
|
@@ -39,17 +39,11 @@
|
||||
<option selected disabled value="">
|
||||
{{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }}
|
||||
</option>
|
||||
<option
|
||||
v-for="t in locationTypes"
|
||||
:value="t"
|
||||
:key="t.id"
|
||||
>
|
||||
<option v-for="t in locationTypes" :value="t" :key="t.id">
|
||||
{{ localizeString(t.title) }}
|
||||
</option>
|
||||
</select>
|
||||
<label>{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_TYPE)
|
||||
}}</label>
|
||||
<label>{{ trans(ACTIVITY_LOCATION_FIELDS_TYPE) }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
@@ -108,10 +102,7 @@
|
||||
</form>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button
|
||||
class="btn btn-save"
|
||||
@click.prevent="saveNewLocation"
|
||||
>
|
||||
<button class="btn btn-save" @click.prevent="saveNewLocation">
|
||||
{{ trans(SAVE) }}
|
||||
</button>
|
||||
</template>
|
||||
@@ -244,8 +235,7 @@ export default {
|
||||
},
|
||||
hasPhonenumber1() {
|
||||
return (
|
||||
this.selected.phonenumber1 !== null &&
|
||||
this.selected.phonenumber1 !== ""
|
||||
this.selected.phonenumber1 !== null && this.selected.phonenumber1 !== ""
|
||||
);
|
||||
},
|
||||
showAddAddress() {
|
||||
|
@@ -49,9 +49,7 @@
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div v-if="actionIsLoading === true">
|
||||
<i
|
||||
class="chill-green fa fa-circle-o-notch fa-spin fa-lg"
|
||||
></i>
|
||||
<i class="chill-green fa fa-circle-o-notch fa-spin fa-lg"></i>
|
||||
</div>
|
||||
|
||||
<span
|
||||
@@ -64,8 +62,7 @@
|
||||
<template
|
||||
v-else-if="
|
||||
socialActionsList.length > 0 &&
|
||||
(socialIssuesSelected.length ||
|
||||
socialActionsSelected.length)
|
||||
(socialIssuesSelected.length || socialActionsSelected.length)
|
||||
"
|
||||
>
|
||||
<div
|
||||
@@ -88,9 +85,7 @@
|
||||
</template>
|
||||
|
||||
<span
|
||||
v-else-if="
|
||||
actionAreLoaded && socialActionsList.length === 0
|
||||
"
|
||||
v-else-if="actionAreLoaded && socialActionsList.length === 0"
|
||||
class="inline-choice chill-no-data-statement mt-3"
|
||||
>
|
||||
{{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }}
|
||||
@@ -169,8 +164,7 @@ export default {
|
||||
/* Add in list the issues already associated (if not yet listed) */
|
||||
this.socialIssuesSelected.forEach((issue) => {
|
||||
if (
|
||||
this.socialIssuesList.filter((i) => i.id === issue.id)
|
||||
.length !== 1
|
||||
this.socialIssuesList.filter((i) => i.id === issue.id).length !== 1
|
||||
) {
|
||||
this.$store.commit("addIssueInList", issue);
|
||||
}
|
||||
|
@@ -10,9 +10,7 @@
|
||||
:value="issue"
|
||||
/>
|
||||
<label class="form-check-label" :for="issue.id">
|
||||
<span class="badge bg-chill-l-gray text-dark">{{
|
||||
issue.text
|
||||
}}</span>
|
||||
<span class="badge bg-chill-l-gray text-dark">{{ issue.text }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</span>
|
||||
|
@@ -68,9 +68,7 @@ export type EventInputCalendarRange = EventInput & {
|
||||
export function isEventInputCalendarRange(
|
||||
toBeDetermined: EventInputCalendarRange | EventInput,
|
||||
): toBeDetermined is EventInputCalendarRange {
|
||||
return (
|
||||
typeof toBeDetermined.is === "string" && toBeDetermined.is === "range"
|
||||
);
|
||||
return typeof toBeDetermined.is === "string" && toBeDetermined.is === "range";
|
||||
}
|
||||
|
||||
export {};
|
||||
|
@@ -61,22 +61,14 @@
|
||||
<label class="input-group-text" for="slotDuration"
|
||||
>Durée des créneaux</label
|
||||
>
|
||||
<select
|
||||
v-model="slotDuration"
|
||||
id="slotDuration"
|
||||
class="form-select"
|
||||
>
|
||||
<select v-model="slotDuration" id="slotDuration" class="form-select">
|
||||
<option value="00:05:00">5 minutes</option>
|
||||
<option value="00:10:00">10 minutes</option>
|
||||
<option value="00:15:00">15 minutes</option>
|
||||
<option value="00:30:00">30 minutes</option>
|
||||
</select>
|
||||
<label class="input-group-text" for="slotMinTime">De</label>
|
||||
<select
|
||||
v-model="slotMinTime"
|
||||
id="slotMinTime"
|
||||
class="form-select"
|
||||
>
|
||||
<select v-model="slotMinTime" id="slotMinTime" class="form-select">
|
||||
<option value="00:00:00">0h</option>
|
||||
<option value="01:00:00">1h</option>
|
||||
<option value="02:00:00">2h</option>
|
||||
@@ -92,11 +84,7 @@
|
||||
<option value="12:00:00">12h</option>
|
||||
</select>
|
||||
<label class="input-group-text" for="slotMaxTime">À</label>
|
||||
<select
|
||||
v-model="slotMaxTime"
|
||||
id="slotMaxTime"
|
||||
class="form-select"
|
||||
>
|
||||
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select">
|
||||
<option value="12:00:00">12h</option>
|
||||
<option value="13:00:00">13h</option>
|
||||
<option value="14:00:00">14h</option>
|
||||
@@ -124,9 +112,7 @@
|
||||
v-model="hideWeekends"
|
||||
/>
|
||||
</span>
|
||||
<label
|
||||
for="showHideWE"
|
||||
class="form-check-label input-group-text"
|
||||
<label for="showHideWE" class="form-check-label input-group-text"
|
||||
>Week-ends</label
|
||||
>
|
||||
</div>
|
||||
@@ -142,9 +128,7 @@
|
||||
<b v-else-if="arg.event.extendedProps.is === 'range'"
|
||||
>{{ arg.timeText }}
|
||||
{{ arg.event.extendedProps.locationName }}
|
||||
<small>{{
|
||||
arg.event.extendedProps.userLabel
|
||||
}}</small></b
|
||||
<small>{{ arg.event.extendedProps.userLabel }}</small></b
|
||||
>
|
||||
<b v-else-if="arg.event.extendedProps.is === 'current'"
|
||||
>{{ arg.timeText }} {{ $t("current_selected") }}
|
||||
@@ -152,9 +136,7 @@
|
||||
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
|
||||
arg.event.title
|
||||
}}</b>
|
||||
<b v-else
|
||||
>{{ arg.timeText }} {{ $t("current_selected") }}
|
||||
</b>
|
||||
<b v-else>{{ arg.timeText }} {{ $t("current_selected") }} </b>
|
||||
</span>
|
||||
</template>
|
||||
</FullCalendar>
|
||||
@@ -268,9 +250,7 @@ export default {
|
||||
this.$store.state.activity.endDate !== null)
|
||||
) {
|
||||
if (
|
||||
!window.confirm(
|
||||
this.$t("change_main_user_will_reset_event_data"),
|
||||
)
|
||||
!window.confirm(this.$t("change_main_user_will_reset_event_data"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -278,13 +258,9 @@ export default {
|
||||
|
||||
// add the previous user, if any, in the previous user list (in use for suggestion)
|
||||
if (null !== this.$store.getters.getMainUser) {
|
||||
const suggestedUids = new Set(
|
||||
this.$data.previousUser.map((u) => u.id),
|
||||
);
|
||||
const suggestedUids = new Set(this.$data.previousUser.map((u) => u.id));
|
||||
if (!suggestedUids.has(this.$store.getters.getMainUser.id)) {
|
||||
this.$data.previousUser.push(
|
||||
this.$store.getters.getMainUser,
|
||||
);
|
||||
this.$data.previousUser.push(this.$store.getters.getMainUser);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,8 +290,7 @@ export default {
|
||||
// show an alert if changing mainUser
|
||||
if (
|
||||
(this.$store.getters.getMainUser !== null &&
|
||||
this.$store.state.me.id !==
|
||||
this.$store.getters.getMainUser.id) ||
|
||||
this.$store.state.me.id !== this.$store.getters.getMainUser.id) ||
|
||||
this.$store.getters.getMainUser === null
|
||||
) {
|
||||
if (!window.confirm(this.$t("will_change_main_user_for_me"))) {
|
||||
@@ -359,9 +334,7 @@ export default {
|
||||
this.$store.getters.getMainUser.id
|
||||
) {
|
||||
if (
|
||||
!window.confirm(
|
||||
this.$t("this_calendar_range_will_change_main_user"),
|
||||
)
|
||||
!window.confirm(this.$t("this_calendar_range_will_change_main_user"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@@ -4,18 +4,9 @@
|
||||
{{ user.text }}
|
||||
<template v-if="invite !== null">
|
||||
<i v-if="invite.status === 'accepted'" class="fa fa-check" />
|
||||
<i
|
||||
v-else-if="invite.status === 'declined'"
|
||||
class="fa fa-times"
|
||||
/>
|
||||
<i
|
||||
v-else-if="invite.status === 'pending'"
|
||||
class="fa fa-question-o"
|
||||
/>
|
||||
<i
|
||||
v-else-if="invite.status === 'tentative'"
|
||||
class="fa fa-question"
|
||||
/>
|
||||
<i v-else-if="invite.status === 'declined'" class="fa fa-times" />
|
||||
<i v-else-if="invite.status === 'pending'" class="fa fa-question-o" />
|
||||
<i v-else-if="invite.status === 'tentative'" class="fa fa-question" />
|
||||
<span v-else="">{{ invite.status }}</span>
|
||||
</template>
|
||||
</span>
|
||||
@@ -69,8 +60,7 @@ export default {
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
backgroundColor: this.$store.getters.getUserData(this.user)
|
||||
.mainColor,
|
||||
backgroundColor: this.$store.getters.getUserData(this.user).mainColor,
|
||||
};
|
||||
},
|
||||
rangeShow: {
|
||||
@@ -81,9 +71,7 @@ export default {
|
||||
});
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.isRangeShownOnCalendarForUser(
|
||||
this.user,
|
||||
);
|
||||
return this.$store.getters.isRangeShownOnCalendarForUser(this.user);
|
||||
},
|
||||
},
|
||||
remoteShow: {
|
||||
@@ -94,9 +82,7 @@ export default {
|
||||
});
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.isRemoteShownOnCalendarForUser(
|
||||
this.user,
|
||||
);
|
||||
return this.$store.getters.isRemoteShownOnCalendarForUser(this.user);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -22,33 +22,25 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
<li v-if="status !== Statuses.ACCEPTED">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="changeStatus(Statuses.ACCEPTED)"
|
||||
><i class="fa fa-check" aria-hidden="true"></i>
|
||||
{{ $t("Accept") }}</a
|
||||
<a class="dropdown-item" @click="changeStatus(Statuses.ACCEPTED)"
|
||||
><i class="fa fa-check" aria-hidden="true"></i> {{ $t("Accept") }}</a
|
||||
>
|
||||
</li>
|
||||
<li v-if="status !== Statuses.DECLINED">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="changeStatus(Statuses.DECLINED)"
|
||||
><i class="fa fa-times" aria-hidden="true"></i>
|
||||
{{ $t("Decline") }}</a
|
||||
<a class="dropdown-item" @click="changeStatus(Statuses.DECLINED)"
|
||||
><i class="fa fa-times" aria-hidden="true"></i> {{ $t("Decline") }}</a
|
||||
>
|
||||
</li>
|
||||
<li v-if="status !== Statuses.TENTATIVELY_ACCEPTED">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)"
|
||||
><i class="fa fa-question"></i>
|
||||
{{ $t("Tentatively_accept") }}</a
|
||||
><i class="fa fa-question"></i> {{ $t("Tentatively_accept") }}</a
|
||||
>
|
||||
</li>
|
||||
<li v-if="status !== Statuses.PENDING">
|
||||
<a class="dropdown-item" @click="changeStatus(Statuses.PENDING)"
|
||||
><i class="fa fa-hourglass-o"></i>
|
||||
{{ $t("Set_pending") }}</a
|
||||
><i class="fa fa-hourglass-o"></i> {{ $t("Set_pending") }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -91,9 +83,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
statusChanged(
|
||||
payload: "accepted" | "declined" | "pending" | "tentative",
|
||||
) {
|
||||
statusChanged(payload: "accepted" | "declined" | "pending" | "tentative") {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
@@ -23,22 +23,14 @@
|
||||
<label class="input-group-text" for="slotDuration"
|
||||
>Durée des créneaux</label
|
||||
>
|
||||
<select
|
||||
v-model="slotDuration"
|
||||
id="slotDuration"
|
||||
class="form-select"
|
||||
>
|
||||
<select v-model="slotDuration" id="slotDuration" class="form-select">
|
||||
<option value="00:05:00">5 minutes</option>
|
||||
<option value="00:10:00">10 minutes</option>
|
||||
<option value="00:15:00">15 minutes</option>
|
||||
<option value="00:30:00">30 minutes</option>
|
||||
</select>
|
||||
<label class="input-group-text" for="slotMinTime">De</label>
|
||||
<select
|
||||
v-model="slotMinTime"
|
||||
id="slotMinTime"
|
||||
class="form-select"
|
||||
>
|
||||
<select v-model="slotMinTime" id="slotMinTime" class="form-select">
|
||||
<option value="00:00:00">0h</option>
|
||||
<option value="01:00:00">1h</option>
|
||||
<option value="02:00:00">2h</option>
|
||||
@@ -54,11 +46,7 @@
|
||||
<option value="12:00:00">12h</option>
|
||||
</select>
|
||||
<label class="input-group-text" for="slotMaxTime">À</label>
|
||||
<select
|
||||
v-model="slotMaxTime"
|
||||
id="slotMaxTime"
|
||||
class="form-select"
|
||||
>
|
||||
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select">
|
||||
<option value="12:00:00">12h</option>
|
||||
<option value="13:00:00">13h</option>
|
||||
<option value="14:00:00">14h</option>
|
||||
@@ -86,9 +74,7 @@
|
||||
v-model="showWeekends"
|
||||
/>
|
||||
</span>
|
||||
<label
|
||||
for="showHideWE"
|
||||
class="form-check-label input-group-text"
|
||||
<label for="showHideWE" class="form-check-label input-group-text"
|
||||
>Week-ends</label
|
||||
>
|
||||
</div>
|
||||
@@ -98,16 +84,12 @@
|
||||
<FullCalendar :options="calendarOptions" ref="calendarRef">
|
||||
<template v-slot:eventContent="{ event }: { event: EventApi }">
|
||||
<span :class="eventClasses">
|
||||
<b v-if="event.extendedProps.is === 'remote'">{{
|
||||
event.title
|
||||
}}</b>
|
||||
<b v-if="event.extendedProps.is === 'remote'">{{ event.title }}</b>
|
||||
<b v-else-if="event.extendedProps.is === 'range'"
|
||||
>{{ formatDate(event.startStr) }} -
|
||||
{{ event.extendedProps.locationName }}</b
|
||||
>
|
||||
<b v-else-if="event.extendedProps.is === 'local'">{{
|
||||
event.title
|
||||
}}</b>
|
||||
<b v-else-if="event.extendedProps.is === 'local'">{{ event.title }}</b>
|
||||
<b v-else>no 'is'</b>
|
||||
<a
|
||||
v-if="event.extendedProps.is === 'range'"
|
||||
@@ -126,11 +108,7 @@
|
||||
<h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-9 col-md-2">
|
||||
<select
|
||||
v-model="dayOrWeek"
|
||||
id="dayOrWeek"
|
||||
class="form-select"
|
||||
>
|
||||
<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") }}
|
||||
@@ -139,27 +117,16 @@
|
||||
</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 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">
|
||||
<input
|
||||
class="form-control"
|
||||
type="date"
|
||||
v-model="copyTo"
|
||||
/>
|
||||
<input class="form-control" type="date" v-model="copyTo" />
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-5 col-md-1">
|
||||
<button
|
||||
class="btn btn-action float-end"
|
||||
@click="copyDay"
|
||||
>
|
||||
<button class="btn btn-action float-end" @click="copyDay">
|
||||
{{ $t("copy_range") }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -171,11 +138,7 @@
|
||||
id="copyFromWeek"
|
||||
class="form-select"
|
||||
>
|
||||
<option
|
||||
v-for="w in lastWeeks"
|
||||
:value="w.value"
|
||||
:key="w.value"
|
||||
>
|
||||
<option v-for="w in lastWeeks" :value="w.value" :key="w.value">
|
||||
{{ w.text }}
|
||||
</option>
|
||||
</select>
|
||||
@@ -184,25 +147,14 @@
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<button class="btn btn-action float-end" @click="copyWeek">
|
||||
{{ $t("copy_range") }}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -41,9 +41,7 @@ const futureStore = function (): Promise<Store<State>> {
|
||||
});
|
||||
|
||||
store.commit("me/setWhoAmi", user, { root: true });
|
||||
store
|
||||
.dispatch("locations/getLocations", null, { root: true })
|
||||
.then((_) => {
|
||||
store.dispatch("locations/getLocations", null, { root: true }).then((_) => {
|
||||
return store.dispatch("locations/getCurrentLocation", null, {
|
||||
root: true,
|
||||
});
|
||||
|
@@ -29,10 +29,7 @@ export default {
|
||||
(state: CalendarLocalsState) =>
|
||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||
for (const range of state.localsLoaded) {
|
||||
if (
|
||||
start.getTime() === range.start &&
|
||||
end.getTime() === range.end
|
||||
) {
|
||||
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -54,10 +51,7 @@ export default {
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
addLoaded(
|
||||
state: CalendarLocalsState,
|
||||
payload: { start: Date; end: Date },
|
||||
) {
|
||||
addLoaded(state: CalendarLocalsState, payload: { start: Date; end: Date }) {
|
||||
state.localsLoaded.push({
|
||||
start: payload.start.getTime(),
|
||||
end: payload.end.getTime(),
|
||||
@@ -85,11 +79,7 @@ export default {
|
||||
end: end,
|
||||
});
|
||||
|
||||
return fetchCalendarLocalForUser(
|
||||
ctx.rootGetters["me/getMe"],
|
||||
start,
|
||||
end,
|
||||
)
|
||||
return fetchCalendarLocalForUser(ctx.rootGetters["me/getMe"], start, end)
|
||||
.then((remotes: CalendarLight[]) => {
|
||||
// to be add when reactivity problem will be solve ?
|
||||
//ctx.commit('addRemotes', remotes);
|
||||
|
@@ -40,10 +40,7 @@ export default {
|
||||
(state: CalendarRangesState) =>
|
||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||
for (const range of state.rangesLoaded) {
|
||||
if (
|
||||
start.getTime() === range.start &&
|
||||
end.getTime() === range.end
|
||||
) {
|
||||
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -110,9 +107,7 @@ export default {
|
||||
state: CalendarRangesState,
|
||||
externalEvents: (EventInput & { id: string })[],
|
||||
) {
|
||||
const toAdd = externalEvents.filter(
|
||||
(r) => !state.rangesIndex.has(r.id),
|
||||
);
|
||||
const toAdd = externalEvents.filter((r) => !state.rangesIndex.has(r.id));
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.rangesIndex.add(r.id);
|
||||
@@ -120,10 +115,7 @@ export default {
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
addLoaded(
|
||||
state: CalendarRangesState,
|
||||
payload: { start: Date; end: Date },
|
||||
) {
|
||||
addLoaded(state: CalendarRangesState, payload: { start: Date; end: Date }) {
|
||||
state.rangesLoaded.push({
|
||||
start: payload.start.getTime(),
|
||||
end: payload.end.getTime(),
|
||||
@@ -142,17 +134,12 @@ export default {
|
||||
},
|
||||
removeRange(state: CalendarRangesState, calendarRangeId: number) {
|
||||
const found = state.ranges.find(
|
||||
(r) =>
|
||||
r.calendarRangeId === calendarRangeId && r.is === "range",
|
||||
(r) => r.calendarRangeId === calendarRangeId && r.is === "range",
|
||||
);
|
||||
|
||||
if (found !== undefined) {
|
||||
state.ranges = state.ranges.filter(
|
||||
(r) =>
|
||||
!(
|
||||
r.calendarRangeId === calendarRangeId &&
|
||||
r.is === "range"
|
||||
),
|
||||
(r) => !(r.calendarRangeId === calendarRangeId && r.is === "range"),
|
||||
);
|
||||
|
||||
if (typeof found.id === "string") {
|
||||
@@ -211,11 +198,7 @@ export default {
|
||||
},
|
||||
createRange(
|
||||
ctx: Context,
|
||||
{
|
||||
start,
|
||||
end,
|
||||
location,
|
||||
}: { start: Date; end: Date; location: Location },
|
||||
{ start, end, location }: { start: Date; end: Date; location: Location },
|
||||
): Promise<null> {
|
||||
const url = `/api/1.0/calendar/calendar-range.json?`;
|
||||
|
||||
@@ -240,11 +223,7 @@ export default {
|
||||
},
|
||||
} as CalendarRangeCreate;
|
||||
|
||||
return makeFetch<CalendarRangeCreate, CalendarRange>(
|
||||
"POST",
|
||||
url,
|
||||
body,
|
||||
)
|
||||
return makeFetch<CalendarRangeCreate, CalendarRange>("POST", url, body)
|
||||
.then((newRange) => {
|
||||
ctx.commit("addRange", newRange);
|
||||
|
||||
@@ -281,11 +260,7 @@ export default {
|
||||
},
|
||||
} as CalendarRangeEdit;
|
||||
|
||||
return makeFetch<CalendarRangeEdit, CalendarRange>(
|
||||
"PATCH",
|
||||
url,
|
||||
body,
|
||||
)
|
||||
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body)
|
||||
.then((range) => {
|
||||
ctx.commit("updateRange", range);
|
||||
return Promise.resolve(null);
|
||||
@@ -310,11 +285,7 @@ export default {
|
||||
},
|
||||
} as CalendarRangeEdit;
|
||||
|
||||
return makeFetch<CalendarRangeEdit, CalendarRange>(
|
||||
"PATCH",
|
||||
url,
|
||||
body,
|
||||
)
|
||||
return makeFetch<CalendarRangeEdit, CalendarRange>("PATCH", url, body)
|
||||
.then((range) => {
|
||||
ctx.commit("updateRange", range);
|
||||
return Promise.resolve(null);
|
||||
@@ -334,20 +305,14 @@ export default {
|
||||
|
||||
for (const r of rangesToCopy) {
|
||||
const start = new Date(ISOToDatetime(r.start) as Date);
|
||||
start.setFullYear(
|
||||
to.getFullYear(),
|
||||
to.getMonth(),
|
||||
to.getDate(),
|
||||
);
|
||||
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||
const end = new Date(ISOToDatetime(r.end) as Date);
|
||||
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||
const location = ctx.rootGetters["locations/getLocationById"](
|
||||
r.locationId,
|
||||
);
|
||||
|
||||
promises.push(
|
||||
ctx.dispatch("createRange", { start, end, location }),
|
||||
);
|
||||
promises.push(ctx.dispatch("createRange", { start, end, location }));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => Promise.resolve(null));
|
||||
@@ -369,9 +334,7 @@ export default {
|
||||
r.locationId,
|
||||
);
|
||||
|
||||
promises.push(
|
||||
ctx.dispatch("createRange", { start, end, location }),
|
||||
);
|
||||
promises.push(ctx.dispatch("createRange", { start, end, location }));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => Promise.resolve(null));
|
||||
|
@@ -29,10 +29,7 @@ export default {
|
||||
(state: CalendarRemotesState) =>
|
||||
({ start, end }: { start: Date; end: Date }): boolean => {
|
||||
for (const range of state.remotesLoaded) {
|
||||
if (
|
||||
start.getTime() === range.start &&
|
||||
end.getTime() === range.end
|
||||
) {
|
||||
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -85,11 +82,7 @@ export default {
|
||||
end: end,
|
||||
});
|
||||
|
||||
return fetchCalendarRemoteForUser(
|
||||
ctx.rootGetters["me/getMe"],
|
||||
start,
|
||||
end,
|
||||
)
|
||||
return fetchCalendarRemoteForUser(ctx.rootGetters["me/getMe"], start, end)
|
||||
.then((remotes: CalendarRemote[]) => {
|
||||
// to be add when reactivity problem will be solve ?
|
||||
//ctx.commit('addRemotes', remotes);
|
||||
|
@@ -112,11 +112,8 @@ export default {
|
||||
|
||||
results.forEach((i) => {
|
||||
if (!users.some((j) => i.user.id === j.id)) {
|
||||
let ratio = Math.floor(
|
||||
users.length / COLORS.length,
|
||||
);
|
||||
let colorIndex =
|
||||
users.length - ratio * COLORS.length;
|
||||
let ratio = Math.floor(users.length / COLORS.length);
|
||||
let colorIndex = users.length - ratio * COLORS.length;
|
||||
users.push({
|
||||
id: i.user.id,
|
||||
username: i.user.username,
|
||||
@@ -153,45 +150,29 @@ export default {
|
||||
(me) =>
|
||||
new Promise((resolve, reject) => {
|
||||
this.users.logged = me;
|
||||
let currentUser = users.find(
|
||||
(u) => u.id === me.id,
|
||||
);
|
||||
let currentUser = users.find((u) => u.id === me.id);
|
||||
this.value = currentUser;
|
||||
|
||||
fetchCalendar(currentUser.id).then(
|
||||
(calendar) =>
|
||||
new Promise(
|
||||
(resolve, reject) => {
|
||||
let results =
|
||||
calendar.results;
|
||||
let events =
|
||||
results.map(
|
||||
(i) => ({
|
||||
start: i
|
||||
.startDate
|
||||
.datetime,
|
||||
end: i
|
||||
.endDate
|
||||
.datetime,
|
||||
}),
|
||||
);
|
||||
let calendarEventsCurrentUser =
|
||||
{
|
||||
new Promise((resolve, reject) => {
|
||||
let results = calendar.results;
|
||||
let events = results.map((i) => ({
|
||||
start: i.startDate.datetime,
|
||||
end: i.endDate.datetime,
|
||||
}));
|
||||
let calendarEventsCurrentUser = {
|
||||
events: events,
|
||||
color: "darkblue",
|
||||
id: 1000,
|
||||
editable: false,
|
||||
};
|
||||
this.calendarEvents.user =
|
||||
calendarEventsCurrentUser;
|
||||
this.calendarEvents.user = calendarEventsCurrentUser;
|
||||
|
||||
this.selectUsers(
|
||||
currentUser,
|
||||
);
|
||||
this.selectUsers(currentUser);
|
||||
|
||||
resolve();
|
||||
},
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
resolve();
|
||||
@@ -209,9 +190,7 @@ export default {
|
||||
return `${value.username}`;
|
||||
},
|
||||
coloriseSelectedValues() {
|
||||
let tags = document.querySelectorAll(
|
||||
"div.multiselect__tags-wrap",
|
||||
)[0];
|
||||
let tags = document.querySelectorAll("div.multiselect__tags-wrap")[0];
|
||||
|
||||
if (tags.hasChildNodes()) {
|
||||
let children = tags.childNodes;
|
||||
@@ -232,8 +211,8 @@ export default {
|
||||
},
|
||||
selectEvents() {
|
||||
let selectedUsersId = this.users.selected.map((a) => a.id);
|
||||
this.calendarEvents.selected = this.calendarEvents.loaded.filter(
|
||||
(a) => selectedUsersId.includes(a.id),
|
||||
this.calendarEvents.selected = this.calendarEvents.loaded.filter((a) =>
|
||||
selectedUsersId.includes(a.id),
|
||||
);
|
||||
},
|
||||
selectUsers(value) {
|
||||
@@ -243,9 +222,7 @@ export default {
|
||||
this.updateEventsSource();
|
||||
},
|
||||
unSelectUsers(value) {
|
||||
this.users.selected = this.users.selected.filter(
|
||||
(a) => a.id != value.id,
|
||||
);
|
||||
this.users.selected = this.users.selected.filter((a) => a.id != value.id);
|
||||
this.selectEvents();
|
||||
this.updateEventsSource();
|
||||
},
|
||||
|
@@ -24,11 +24,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class CalendarForShortMessageProvider
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CalendarRepository $calendarRepository,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly RangeGeneratorInterface $rangeGenerator,
|
||||
) {}
|
||||
public function __construct(private readonly CalendarRepository $calendarRepository, private readonly EntityManagerInterface $em, private readonly RangeGeneratorInterface $rangeGenerator) {}
|
||||
|
||||
/**
|
||||
* Generate calendars instance.
|
||||
|
@@ -21,6 +21,7 @@ namespace Chill\CalendarBundle\Tests\Service\ShortMessageNotification;
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -81,16 +82,10 @@ final class CalendarForShortMessageProviderTest extends TestCase
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$em->clear()->shouldBeCalled();
|
||||
|
||||
$calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
|
||||
$calendarRangeGenerator->generateRange(Argument::any())->willReturn([
|
||||
'startDate' => new \DateTimeImmutable('yesterday'),
|
||||
'endDate' => new \DateTimeImmutable('now'),
|
||||
]);
|
||||
|
||||
$provider = new CalendarForShortMessageProvider(
|
||||
$calendarRepository->reveal(),
|
||||
$em->reveal(),
|
||||
$calendarRangeGenerator->reveal(),
|
||||
new DefaultRangeGenerator()
|
||||
);
|
||||
|
||||
$calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
|
||||
@@ -108,32 +103,26 @@ final class CalendarForShortMessageProviderTest extends TestCase
|
||||
Argument::type(\DateTimeImmutable::class),
|
||||
Argument::type('int'),
|
||||
Argument::exact(0)
|
||||
)->will(static fn ($args) => array_fill(0, 10, new Calendar()))->shouldBeCalledTimes(1);
|
||||
)->will(static fn ($args) => array_fill(0, 1, new Calendar()))->shouldBeCalledTimes(1);
|
||||
$calendarRepository->findByNotificationAvailable(
|
||||
Argument::type(\DateTimeImmutable::class),
|
||||
Argument::type(\DateTimeImmutable::class),
|
||||
Argument::type('int'),
|
||||
Argument::exact(10)
|
||||
Argument::not(0)
|
||||
)->will(static fn ($args) => [])->shouldBeCalledTimes(1);
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$em->clear()->shouldBeCalled();
|
||||
|
||||
$calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
|
||||
$calendarRangeGenerator->generateRange(Argument::any())->willReturn([
|
||||
'startDate' => new \DateTimeImmutable('yesterday'),
|
||||
'endDate' => new \DateTimeImmutable('now'),
|
||||
]);
|
||||
|
||||
$provider = new CalendarForShortMessageProvider(
|
||||
$calendarRepository->reveal(),
|
||||
$em->reveal(),
|
||||
$calendarRangeGenerator->reveal(),
|
||||
new DefaultRangeGenerator()
|
||||
);
|
||||
|
||||
$calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
|
||||
|
||||
$this->assertEquals(10, \count($calendars));
|
||||
$this->assertEquals(1, \count($calendars));
|
||||
$this->assertContainsOnly(Calendar::class, $calendars);
|
||||
}
|
||||
}
|
||||
|
@@ -20,10 +20,7 @@
|
||||
</option>
|
||||
<template v-for="t in templates" :key="t.id">
|
||||
<option :value="t.id">
|
||||
{{
|
||||
localizeString(t.name) ||
|
||||
"Aucun nom défini"
|
||||
}}
|
||||
{{ localizeString(t.name) || "Aucun nom défini" }}
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
@@ -31,9 +28,7 @@
|
||||
v-if="canGenerate"
|
||||
class="btn btn-update btn-sm change-icon"
|
||||
:href="buildUrlGenerate"
|
||||
@click.prevent="
|
||||
clickGenerate($event, buildUrlGenerate)
|
||||
"
|
||||
@click.prevent="clickGenerate($event, buildUrlGenerate)"
|
||||
><i class="fa fa-fw fa-cog"
|
||||
/></a>
|
||||
<a
|
||||
|
@@ -13,8 +13,9 @@ const startApp = (
|
||||
|
||||
const inputTitle = collectionEntry?.querySelector("input[type='text']");
|
||||
|
||||
const input_stored_object: HTMLInputElement | null =
|
||||
divElement.querySelector("input[data-stored-object]");
|
||||
const input_stored_object: HTMLInputElement | null = divElement.querySelector(
|
||||
"input[data-stored-object]",
|
||||
);
|
||||
if (null === input_stored_object) {
|
||||
throw new Error("input to stored object not found");
|
||||
}
|
||||
@@ -53,9 +54,7 @@ const startApp = (
|
||||
console.log("version added", stored_object_version);
|
||||
this.$data.existingDoc = stored_object;
|
||||
this.$data.existingDoc.currentVersion = stored_object_version;
|
||||
input_stored_object.value = JSON.stringify(
|
||||
this.$data.existingDoc,
|
||||
);
|
||||
input_stored_object.value = JSON.stringify(this.$data.existingDoc);
|
||||
if (this.$data.inputTitle) {
|
||||
if (!this.$data.inputTitle?.value) {
|
||||
this.$data.inputTitle.value = file_name;
|
||||
|
@@ -49,9 +49,7 @@
|
||||
<li v-if="isHistoryViewable">
|
||||
<history-button
|
||||
:stored-object="props.storedObject"
|
||||
:can-edit="
|
||||
canEdit && props.storedObject._permissions.canEdit
|
||||
"
|
||||
:can-edit="canEdit && props.storedObject._permissions.canEdit"
|
||||
></history-button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -129,9 +127,7 @@ const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
|
||||
canDownload: true,
|
||||
canConvertPdf: true,
|
||||
returnPath:
|
||||
window.location.pathname +
|
||||
window.location.search +
|
||||
window.location.hash,
|
||||
window.location.pathname + window.location.search + window.location.hash,
|
||||
});
|
||||
|
||||
/**
|
||||
|
@@ -29,9 +29,7 @@
|
||||
</modal>
|
||||
</teleport>
|
||||
<div class="col-12 m-auto sticky-top">
|
||||
<div
|
||||
class="row justify-content-center border-bottom pdf-tools d-md-none"
|
||||
>
|
||||
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
|
||||
<div class="col-5 text-center turn-page">
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
@@ -92,10 +90,7 @@
|
||||
v-if="signature.zones.length === 1 && signedState !== 'signed'"
|
||||
class="col-5 p-0 text-center turnSignature"
|
||||
>
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
@click="goToSignatureZoneUnique"
|
||||
>
|
||||
<button class="btn btn-light btn-sm" @click="goToSignatureZoneUnique">
|
||||
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -150,10 +145,7 @@
|
||||
:title="trans(SIGNATURES_ADD_SIGN_ZONE)"
|
||||
>
|
||||
<template v-if="canvasEvent === 'add'">
|
||||
<div
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
>
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -207,10 +199,7 @@
|
||||
v-if="signature.zones.length === 1 && signedState !== 'signed'"
|
||||
class="col-4 d-xl-none text-center turnSignature p-0"
|
||||
>
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
@click="goToSignatureZoneUnique"
|
||||
>
|
||||
<button class="btn btn-light btn-sm" @click="goToSignatureZoneUnique">
|
||||
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -238,10 +227,7 @@
|
||||
v-if="signature.zones.length === 1 && signedState !== 'signed'"
|
||||
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
|
||||
>
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
@click="goToSignatureZoneUnique"
|
||||
>
|
||||
<button class="btn btn-light btn-sm" @click="goToSignatureZoneUnique">
|
||||
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -299,10 +285,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ trans(SIGNATURES_CLICK_ON_DOCUMENT) }}
|
||||
<div
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
>
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -562,14 +545,8 @@ const addCanvasEvents = () => {
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const canvas = document.querySelectorAll(
|
||||
"canvas",
|
||||
)[0] as HTMLCanvasElement;
|
||||
canvas.addEventListener(
|
||||
"pointerup",
|
||||
(e) => canvasClick(e, canvas),
|
||||
false,
|
||||
);
|
||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||
canvas.addEventListener("pointerup", (e) => canvasClick(e, canvas), false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -605,11 +582,7 @@ const hitSignature = (
|
||||
scaleYToCanvas(zone.y, canvas.height, zone.PDFPage.height) <
|
||||
xy[1] &&
|
||||
xy[1] <
|
||||
scaleYToCanvas(
|
||||
zone.height - zone.y,
|
||||
canvas.height,
|
||||
zone.PDFPage.height,
|
||||
) +
|
||||
scaleYToCanvas(zone.height - zone.y, canvas.height, zone.PDFPage.height) +
|
||||
zone.PDFPage.height * zoom.value;
|
||||
|
||||
const selectZone = async (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
||||
@@ -625,8 +598,7 @@ const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||||
signature.zones
|
||||
.filter(
|
||||
(z) =>
|
||||
(z.PDFPage.index + 1 === getCanvasId(canvas) &&
|
||||
multiPage.value) ||
|
||||
(z.PDFPage.index + 1 === getCanvasId(canvas) && multiPage.value) ||
|
||||
(z.PDFPage.index + 1 === page.value && !multiPage.value),
|
||||
)
|
||||
.map((z) => {
|
||||
|
@@ -153,12 +153,10 @@ const handleFile = async (file: File): Promise<void> => {
|
||||
</p>
|
||||
<!-- todo i18n -->
|
||||
<p v-if="has_existing_doc">
|
||||
Déposez un document ou cliquez ici pour remplacer le document
|
||||
existant
|
||||
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
|
||||
Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="waiting">
|
||||
|
@@ -35,9 +35,7 @@ async function download_and_open(event: Event): Promise<void> {
|
||||
if (null === state.content) {
|
||||
event.preventDefault();
|
||||
|
||||
const raw = await download_doc(
|
||||
build_convert_link(props.storedObject.uuid),
|
||||
);
|
||||
const raw = await download_doc(build_convert_link(props.storedObject.uuid));
|
||||
state.content = window.URL.createObjectURL(raw);
|
||||
|
||||
button.href = window.URL.createObjectURL(raw);
|
||||
|
@@ -42,9 +42,7 @@ const editionUntilFormatted = computed<string>(() => {
|
||||
<modal v-if="state.modalOpened" @close="state.modalOpened = false">
|
||||
<template v-slot:body>
|
||||
<div class="desktop-edit">
|
||||
<p class="center">
|
||||
Veuillez enregistrer vos modifications avant le
|
||||
</p>
|
||||
<p class="center">Veuillez enregistrer vos modifications avant le</p>
|
||||
<p>
|
||||
<strong>{{ editionUntilFormatted }}</strong>
|
||||
</p>
|
||||
@@ -57,23 +55,21 @@ const editionUntilFormatted = computed<string>(() => {
|
||||
|
||||
<p>
|
||||
<small
|
||||
>Le document peut être édité uniquement en utilisant
|
||||
Libre Office.</small
|
||||
>Le document peut être édité uniquement en utilisant Libre
|
||||
Office.</small
|
||||
>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<small
|
||||
>En cas d'échec lors de l'enregistrement, sauver le
|
||||
document sur le poste de travail avant de le déposer
|
||||
à nouveau ici.</small
|
||||
>En cas d'échec lors de l'enregistrement, sauver le document sur
|
||||
le poste de travail avant de le déposer à nouveau ici.</small
|
||||
>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<small
|
||||
>Vous pouvez naviguez sur d'autres pages pendant
|
||||
l'édition.</small
|
||||
>Vous pouvez naviguez sur d'autres pages pendant l'édition.</small
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
@@ -95,10 +95,7 @@ async function download_and_open(): Promise<void> {
|
||||
let raw;
|
||||
|
||||
try {
|
||||
raw = await download_and_decrypt_doc(
|
||||
props.storedObject,
|
||||
props.atVersion,
|
||||
);
|
||||
raw = await download_and_decrypt_doc(props.storedObject, props.atVersion);
|
||||
} catch (e) {
|
||||
console.error("error while downloading and decrypting document");
|
||||
console.error(e);
|
||||
|
@@ -49,8 +49,7 @@ const isRestored = computed<boolean>(
|
||||
);
|
||||
|
||||
const isDuplicated = computed<boolean>(
|
||||
() =>
|
||||
props.version.version === 0 && null !== props.version["from-restored"],
|
||||
() => props.version.version === 0 && null !== props.version["from-restored"],
|
||||
);
|
||||
|
||||
const classes = computed<{
|
||||
@@ -70,16 +69,9 @@ const classes = computed<{
|
||||
<div :class="classes">
|
||||
<div
|
||||
class="col-12 tags"
|
||||
v-if="
|
||||
isCurrent ||
|
||||
isKeptBeforeConversion ||
|
||||
isRestored ||
|
||||
isDuplicated
|
||||
"
|
||||
>
|
||||
<span class="badge bg-success" v-if="isCurrent"
|
||||
>Version actuelle</span
|
||||
v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated"
|
||||
>
|
||||
<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
|
||||
>
|
||||
@@ -96,21 +88,17 @@ const classes = computed<{
|
||||
<span
|
||||
><strong> #{{ version.version + 1 }} </strong></span
|
||||
>
|
||||
<template
|
||||
v-if="version.createdBy !== null && version.createdAt !== null"
|
||||
<template v-if="version.createdBy !== null && version.createdAt !== null"
|
||||
><strong v-if="version.version == 0">créé par</strong
|
||||
><strong v-else>modifié par</strong>
|
||||
<span class="badge-user"
|
||||
><UserRenderBoxBadge
|
||||
:user="version.createdBy"
|
||||
></UserRenderBoxBadge
|
||||
><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge
|
||||
></span>
|
||||
<strong>à</strong>
|
||||
{{
|
||||
$d(ISOToDatetime(version.createdAt.datetime8601), "long")
|
||||
}}</template
|
||||
><template
|
||||
v-if="version.createdBy === null && version.createdAt !== null"
|
||||
><template v-if="version.createdBy === null && version.createdAt !== null"
|
||||
><strong v-if="version.version == 0">Créé le</strong
|
||||
><strong v-else>modifié le</strong>
|
||||
{{
|
||||
|
@@ -2,9 +2,7 @@
|
||||
<a
|
||||
:class="Object.assign(props.classes, { btn: true })"
|
||||
@click="beforeLeave($event)"
|
||||
:href="
|
||||
build_wopi_editor_link(props.storedObject.uuid, props.returnPath)
|
||||
"
|
||||
:href="build_wopi_editor_link(props.storedObject.uuid, props.returnPath)"
|
||||
>
|
||||
<i class="fa fa-paragraph"></i>
|
||||
Editer en ligne
|
||||
|
@@ -145,9 +145,7 @@ async function download_info_link(
|
||||
function build_wopi_editor_link(uuid: string, returnPath?: string) {
|
||||
if (returnPath === undefined) {
|
||||
returnPath =
|
||||
window.location.pathname +
|
||||
window.location.search +
|
||||
window.location.hash;
|
||||
window.location.pathname + window.location.search + window.location.hash;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -186,10 +184,7 @@ async function download_and_decrypt_doc(
|
||||
) {
|
||||
downloadInfo = storedObject._links.downloadLink;
|
||||
} else {
|
||||
downloadInfo = await download_info_link(
|
||||
storedObject,
|
||||
atVersionToDownload,
|
||||
);
|
||||
downloadInfo = await download_info_link(storedObject, atVersionToDownload);
|
||||
}
|
||||
|
||||
const rawResponse = await window.fetch(downloadInfo.url);
|
||||
@@ -244,10 +239,7 @@ async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob> {
|
||||
}
|
||||
|
||||
if (storedObject.currentVersion?.type === "application/pdf") {
|
||||
return download_and_decrypt_doc(
|
||||
storedObject,
|
||||
storedObject.currentVersion,
|
||||
);
|
||||
return download_and_decrypt_doc(storedObject, storedObject.currentVersion);
|
||||
}
|
||||
|
||||
const convertLink = build_convert_link(storedObject.uuid);
|
||||
|
@@ -20,7 +20,6 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompiler
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
|
||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Chill\MainBundle\Search\SearchApiInterface;
|
||||
@@ -62,8 +61,6 @@ class ChillMainBundle extends Bundle
|
||||
->addTag('chill_main.entity_info_provider');
|
||||
$container->registerForAutoconfiguration(ProvideRoleInterface::class)
|
||||
->addTag('chill_main.provide_role');
|
||||
$container->registerForAutoconfiguration(NotificationFlagProviderInterface::class)
|
||||
->addTag('chill_main.notification_flag_provider');
|
||||
|
||||
$container->addCompilerPass(new SearchableServicesCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new ConfigConsistencyCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
|
@@ -1,50 +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\Controller;
|
||||
|
||||
use Chill\MainBundle\Repository\AddressReferenceRepositoryInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final readonly class AddressReferenceAggregatedApiController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private AddressReferenceRepositoryInterface $addressReferenceRepository,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/api/1.0/main/address-reference/aggregated/search')]
|
||||
public function search(Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted('IS_AUTHENTICATED')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
if (!$request->query->has('q')) {
|
||||
throw new BadRequestHttpException('Parameter "q" is required.');
|
||||
}
|
||||
|
||||
$q = trim($request->query->get('q'));
|
||||
|
||||
if ('' === $q) {
|
||||
throw new BadRequestHttpException('Parameter "q" is required and cannot be empty.');
|
||||
}
|
||||
|
||||
$result = $this->addressReferenceRepository->findAggregatedBySearchString($q);
|
||||
|
||||
return new JsonResponse(iterator_to_array($result));
|
||||
}
|
||||
}
|
@@ -16,7 +16,6 @@ use Chill\MainBundle\Entity\NotificationComment;
|
||||
use Chill\MainBundle\Form\NotificationCommentType;
|
||||
use Chill\MainBundle\Form\NotificationType;
|
||||
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
|
||||
use Chill\MainBundle\Notification\FlagProviders\NotificationByUserFlagProvider;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerManager;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
@@ -58,8 +57,7 @@ class NotificationController extends AbstractController
|
||||
$notification
|
||||
->setRelatedEntityClass($request->query->get('entityClass'))
|
||||
->setRelatedEntityId($request->query->getInt('entityId'))
|
||||
->setSender($this->security->getUser())
|
||||
->setType(NotificationByUserFlagProvider::FLAG);
|
||||
->setSender($this->security->getUser());
|
||||
|
||||
$tos = $request->query->all('tos');
|
||||
|
||||
|
@@ -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\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Repository\PostalCodeForAddressReferenceRepositoryInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final readonly class PostalCodeForAddressReferenceApiController
|
||||
{
|
||||
public function __construct(
|
||||
private PostalCodeForAddressReferenceRepositoryInterface $postalCodeForAddressReferenceRepository,
|
||||
private Security $security,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/main/address-reference/postal-code/search')]
|
||||
public function findPostalCodeBySearch(Request $request): JsonResponse
|
||||
{
|
||||
|
||||
if (!$this->security->isGranted('IS_AUTHENTICATED')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$search = $request->query->get('q');
|
||||
|
||||
if (null === $search || '' === trim($search)) {
|
||||
throw new BadRequestHttpException('No search query provided');
|
||||
}
|
||||
|
||||
$postalCodes = iterator_to_array($this->postalCodeForAddressReferenceRepository->findPostalCode($search));
|
||||
|
||||
return new JsonResponse($postalCodes, json: false);
|
||||
}
|
||||
}
|
@@ -11,11 +11,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Form\UserProfileType;
|
||||
use Chill\MainBundle\Form\UserPhonenumberType;
|
||||
use Chill\MainBundle\Security\ChillSecurity;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
@@ -38,19 +41,16 @@ final class UserProfileController extends AbstractController
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
$editForm = $this->createForm(UserProfileType::class, $user);
|
||||
|
||||
$editForm->get('notificationFlags')->setData($user->getNotificationFlags());
|
||||
|
||||
$editForm = $this->createPhonenumberEditForm($user);
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$notificationFlagsData = $editForm->get('notificationFlags')->getData();
|
||||
$user->setNotificationFlags($notificationFlagsData);
|
||||
$phonenumber = $editForm->get('phonenumber')->getData();
|
||||
|
||||
$em = $this->managerRegistry->getManager();
|
||||
$em->flush();
|
||||
$this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!'));
|
||||
$user->setPhonenumber($phonenumber);
|
||||
|
||||
$this->managerRegistry->getManager()->flush();
|
||||
$this->addFlash('success', $this->translator->trans('user.profile.Phonenumber successfully updated!'));
|
||||
|
||||
return $this->redirectToRoute('chill_main_user_profile');
|
||||
}
|
||||
@@ -60,4 +60,13 @@ final class UserProfileController extends AbstractController
|
||||
'form' => $editForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createPhonenumberEditForm(UserInterface $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserPhonenumberType::class,
|
||||
$user,
|
||||
)
|
||||
->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
|
||||
}
|
||||
}
|
||||
|
@@ -14,7 +14,6 @@ namespace Chill\MainBundle\Entity;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
@@ -22,10 +21,10 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
#[ORM\Entity]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ORM\Table(name: 'chill_main_notification')]
|
||||
#[ORM\Index(columns: ['relatedentityclass', 'relatedentityid'], name: 'chill_main_notification_related_entity_idx')]
|
||||
#[ORM\Index(name: 'chill_main_notification_related_entity_idx', columns: ['relatedentityclass', 'relatedentityid'])]
|
||||
class Notification implements TrackUpdateInterface
|
||||
{
|
||||
#[ORM\Column(type: Types::TEXT, nullable: false)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false)]
|
||||
private string $accessKey;
|
||||
|
||||
private array $addedAddresses = [];
|
||||
@@ -37,19 +36,12 @@ class Notification implements TrackUpdateInterface
|
||||
#[ORM\JoinTable(name: 'chill_main_notification_addresses_user')]
|
||||
private Collection $addressees;
|
||||
|
||||
/**
|
||||
* @var Collection<int, UserGroup>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: UserGroup::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_notification_addressee_user_group')]
|
||||
private Collection $addresseeUserGroups;
|
||||
|
||||
/**
|
||||
* a list of destinee which will receive notifications.
|
||||
*
|
||||
* @var array|string[]
|
||||
*/
|
||||
#[ORM\Column(type: Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
|
||||
private array $addressesEmails = [];
|
||||
|
||||
/**
|
||||
@@ -68,21 +60,21 @@ class Notification implements TrackUpdateInterface
|
||||
#[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
|
||||
private Collection $comments;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
|
||||
private \DateTimeImmutable $date;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
|
||||
private string $message = '';
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
|
||||
private string $relatedEntityClass = '';
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
private int $relatedEntityId;
|
||||
|
||||
private array $removedAddresses = [];
|
||||
@@ -92,7 +84,7 @@ class Notification implements TrackUpdateInterface
|
||||
private ?User $sender = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'notification.Title must be defined')]
|
||||
#[ORM\Column(type: Types::TEXT, options: ['default' => ''])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
|
||||
private string $title = '';
|
||||
|
||||
/**
|
||||
@@ -102,46 +94,31 @@ class Notification implements TrackUpdateInterface
|
||||
#[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]
|
||||
private Collection $unreadBy;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
|
||||
private ?\DateTimeImmutable $updatedAt = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
private ?User $updatedBy = null;
|
||||
|
||||
#[ORM\Column(name: 'type', type: Types::STRING, nullable: true)]
|
||||
private string $type = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->addressees = new ArrayCollection();
|
||||
$this->addresseeUserGroups = new ArrayCollection();
|
||||
$this->unreadBy = new ArrayCollection();
|
||||
$this->comments = new ArrayCollection();
|
||||
$this->setDate(new \DateTimeImmutable());
|
||||
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(24));
|
||||
}
|
||||
|
||||
public function addAddressee(User|UserGroup $addressee): self
|
||||
public function addAddressee(User $addressee): self
|
||||
{
|
||||
if ($addressee instanceof User) {
|
||||
if (!$this->addressees->contains($addressee)) {
|
||||
$this->addressees->add($addressee);
|
||||
$this->addressees[] = $addressee;
|
||||
$this->addedAddresses[] = $addressee;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (!$this->addresseeUserGroups->contains($addressee)) {
|
||||
$this->addresseeUserGroups->add($addressee);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function addAddressesEmail(string $email)
|
||||
{
|
||||
if (!\in_array($email, $this->addressesEmails, true)) {
|
||||
@@ -175,23 +152,13 @@ class Notification implements TrackUpdateInterface
|
||||
#[Assert\Callback]
|
||||
public function assertCountAddresses(ExecutionContextInterface $context, $payload): void
|
||||
{
|
||||
if (0 === (\count($this->getAddresseeUserGroups()) + \count($this->getAddressees()))) {
|
||||
if (0 === (\count($this->getAddressesEmails()) + \count($this->getAddressees()))) {
|
||||
$context->buildViolation('notification.At least one addressee')
|
||||
->atPath('addressees')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
public function getAddresseeUserGroups(): Collection
|
||||
{
|
||||
return $this->addresseeUserGroups;
|
||||
}
|
||||
|
||||
public function setAddresseeUserGroups(Collection $addresseeUserGroups): void
|
||||
{
|
||||
$this->addresseeUserGroups = $addresseeUserGroups;
|
||||
}
|
||||
|
||||
public function getAccessKey(): string
|
||||
{
|
||||
return $this->accessKey;
|
||||
@@ -215,23 +182,6 @@ class Notification implements TrackUpdateInterface
|
||||
return $this->addressees;
|
||||
}
|
||||
|
||||
public function getAllAddressees(): array
|
||||
{
|
||||
$allUsers = [];
|
||||
|
||||
foreach ($this->getAddressees() as $user) {
|
||||
$allUsers[$user->getId()] = $user;
|
||||
}
|
||||
|
||||
foreach ($this->getAddresseeUserGroups() as $userGroup) {
|
||||
foreach ($userGroup->getUsers() as $user) {
|
||||
$allUsers[$user->getId()] = $user;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($allUsers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|string[]
|
||||
*/
|
||||
@@ -353,17 +303,11 @@ class Notification implements TrackUpdateInterface
|
||||
$this->addressesOnLoad = null;
|
||||
}
|
||||
|
||||
public function removeAddressee(User|UserGroup $addressee): self
|
||||
public function removeAddressee(User $addressee): self
|
||||
{
|
||||
if ($addressee instanceof User) {
|
||||
if ($this->addressees->contains($addressee)) {
|
||||
$this->addressees->removeElement($addressee);
|
||||
|
||||
return $this;
|
||||
if ($this->addressees->removeElement($addressee)) {
|
||||
$this->removedAddresses[] = $addressee;
|
||||
}
|
||||
}
|
||||
|
||||
$this->addresseeUserGroups->removeElement($addressee);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -434,7 +378,7 @@ class Notification implements TrackUpdateInterface
|
||||
|
||||
public function setUpdatedAt(\DateTimeInterface $datetime): self
|
||||
{
|
||||
$this->updatedAt = \DateTimeImmutable::createFromInterface($datetime);
|
||||
$this->updatedAt = $datetime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -445,16 +389,4 @@ class Notification implements TrackUpdateInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setType(string $type): self
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
|
@@ -34,9 +34,6 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||
#[ORM\Table(name: 'users')]
|
||||
class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
public const NOTIF_FLAG_IMMEDIATE_EMAIL = 'immediate-email';
|
||||
public const NOTIF_FLAG_DAILY_DIGEST = 'daily-digest';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||
@@ -119,12 +116,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
#[PhonenumberConstraint]
|
||||
private ?PhoneNumber $phonenumber = null;
|
||||
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||
private array $notificationFlags = [];
|
||||
|
||||
/**
|
||||
* User constructor.
|
||||
*/
|
||||
@@ -632,47 +623,4 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getNotificationFlags(): array
|
||||
{
|
||||
return $this->notificationFlags;
|
||||
}
|
||||
|
||||
public function setNotificationFlags(array $notificationFlags)
|
||||
{
|
||||
$this->notificationFlags = $notificationFlags;
|
||||
}
|
||||
|
||||
public function getNotificationFlagData(string $flag): array
|
||||
{
|
||||
return $this->notificationFlags[$flag] ?? [];
|
||||
}
|
||||
|
||||
public function setNotificationFlagData(string $flag, array $data): void
|
||||
{
|
||||
$this->notificationFlags[$flag] = $data;
|
||||
}
|
||||
|
||||
public function isNotificationSendImmediately(string $type): bool
|
||||
{
|
||||
if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isNotificationDailyDigest(string $type): bool
|
||||
{
|
||||
if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getLocale(): string
|
||||
{
|
||||
return 'fr';
|
||||
}
|
||||
}
|
||||
|
@@ -27,6 +27,8 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
// command to get the report with curl : curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff
|
||||
|
||||
/**
|
||||
* Create a CSV List for the export.
|
||||
*/
|
||||
|
@@ -1,75 +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\Form\DataMapper;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\Form\DataMapperInterface;
|
||||
|
||||
final readonly class NotificationFlagDataMapper implements DataMapperInterface
|
||||
{
|
||||
public function __construct(private array $notificationFlagProviders) {}
|
||||
|
||||
public function mapDataToForms($viewData, $forms): void
|
||||
{
|
||||
if (null === $viewData) {
|
||||
$viewData = [];
|
||||
}
|
||||
|
||||
$formsArray = iterator_to_array($forms);
|
||||
|
||||
foreach ($this->notificationFlagProviders as $flagProvider) {
|
||||
$flag = $flagProvider->getFlag();
|
||||
|
||||
if (isset($formsArray[$flag])) {
|
||||
$flagForm = $formsArray[$flag];
|
||||
|
||||
$immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true)
|
||||
|| !array_key_exists($flag, $viewData);
|
||||
$dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true);
|
||||
|
||||
if ($flagForm->has('immediate_email')) {
|
||||
$flagForm->get('immediate_email')->setData($immediateEmailChecked);
|
||||
}
|
||||
if ($flagForm->has('daily_email')) {
|
||||
$flagForm->get('daily_email')->setData($dailyEmailChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function mapFormsToData($forms, &$viewData): void
|
||||
{
|
||||
$formsArray = iterator_to_array($forms);
|
||||
$viewData = [];
|
||||
|
||||
foreach ($this->notificationFlagProviders as $flagProvider) {
|
||||
$flag = $flagProvider->getFlag();
|
||||
|
||||
if (isset($formsArray[$flag])) {
|
||||
$flagForm = $formsArray[$flag];
|
||||
$viewData[$flag] = [];
|
||||
|
||||
if (true === $flagForm['immediate_email']->getData()) {
|
||||
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
|
||||
}
|
||||
|
||||
if (true === $flagForm['daily_email']->getData()) {
|
||||
$viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST;
|
||||
}
|
||||
|
||||
if ([] === $viewData[$flag]) {
|
||||
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,12 +12,17 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Email;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
|
||||
class NotificationType extends AbstractType
|
||||
{
|
||||
@@ -28,14 +33,29 @@ class NotificationType extends AbstractType
|
||||
'label' => 'Title',
|
||||
'required' => true,
|
||||
])
|
||||
->add('addressees', PickUserGroupOrUserDynamicType::class, [
|
||||
->add('addressees', PickUserDynamicType::class, [
|
||||
'multiple' => true,
|
||||
'label' => 'notification.Pick user or user group',
|
||||
'empty_data' => '[]',
|
||||
'required' => true,
|
||||
'required' => false,
|
||||
])
|
||||
->add('message', ChillTextareaType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('addressesEmails', ChillCollectionType::class, [
|
||||
'label' => 'notification.dest by email',
|
||||
'help' => 'notification.dest by email help',
|
||||
'by_reference' => false,
|
||||
'allow_add' => true,
|
||||
'allow_delete' => true,
|
||||
'entry_type' => EmailType::class,
|
||||
'button_add_label' => 'notification.Add an email',
|
||||
'button_remove_label' => 'notification.Remove an email',
|
||||
'empty_collection_explain' => 'notification.Any email',
|
||||
'entry_options' => [
|
||||
'constraints' => [
|
||||
new NotNull(), new NotBlank(), new Email(),
|
||||
],
|
||||
'label' => 'Email',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -1,63 +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\Form\Type;
|
||||
|
||||
use Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper;
|
||||
use Chill\MainBundle\Notification\NotificationFlagManager;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class NotificationFlagsType extends AbstractType
|
||||
{
|
||||
private readonly array $notificationFlagProviders;
|
||||
|
||||
public function __construct(NotificationFlagManager $notificationFlagManager)
|
||||
{
|
||||
$this->notificationFlagProviders = $notificationFlagManager->getAllNotificationFlagProviders();
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders));
|
||||
|
||||
foreach ($this->notificationFlagProviders as $flagProvider) {
|
||||
$flag = $flagProvider->getFlag();
|
||||
$builder->add($flag, FormType::class, [
|
||||
'label' => $flagProvider->getLabel(),
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->get($flag)
|
||||
->add('immediate_email', CheckboxType::class, [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('daily_email', CheckboxType::class, [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => null,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -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\Form;
|
||||
|
||||
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
||||
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class UserProfileType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('phonenumber', ChillPhoneNumberType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('notificationFlags', NotificationFlagsType::class, [
|
||||
'label' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => \Chill\MainBundle\Entity\User::class,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -1,102 +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\Notification\Email;
|
||||
|
||||
use Chill\MainBundle\Cron\CronJobInterface;
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
readonly class DailyNotificationDigestCronjob implements CronJobInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ClockInterface $clock,
|
||||
private Connection $connection,
|
||||
private MessageBusInterface $messageBus,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||
{
|
||||
$now = $this->clock->now();
|
||||
|
||||
if (null !== $cronJobExecution && $now->sub(new \DateInterval('PT23H45M')) < $cronJobExecution->getLastStart()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Run between 6 and 9 AM
|
||||
return in_array((int) $now->format('H'), [6, 7, 8], true);
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return 'daily-notification-digest';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateInvalidOperationException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function run(array $lastExecutionData): ?array
|
||||
{
|
||||
$now = $this->clock->now();
|
||||
if (isset($lastExecutionData['last_execution'])) {
|
||||
$lastExecution = \DateTimeImmutable::createFromFormat(
|
||||
\DateTimeImmutable::ATOM,
|
||||
$lastExecutionData['last_execution']
|
||||
);
|
||||
} else {
|
||||
$lastExecution = $now->sub(new \DateInterval('P1D'));
|
||||
}
|
||||
|
||||
// Get distinct users who received notifications since the last execution
|
||||
$sql = <<<'SQL'
|
||||
SELECT DISTINCT cmnau.user_id
|
||||
FROM chill_main_notification cmn
|
||||
JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id
|
||||
WHERE cmn.date >= :lastExecution AND cmn.date <= :now
|
||||
SQL;
|
||||
|
||||
$sqlStatement = $this->connection->prepare($sql);
|
||||
$sqlStatement->bindValue('lastExecution', $lastExecution->format(\DateTimeInterface::RFC3339));
|
||||
$sqlStatement->bindValue('now', $now->format(\DateTimeInterface::RFC3339));
|
||||
$result = $sqlStatement->executeQuery();
|
||||
|
||||
$count = 0;
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
$userId = (int) $row['user_id'];
|
||||
|
||||
$message = new ScheduleDailyNotificationDigestMessage(
|
||||
$userId,
|
||||
$lastExecution,
|
||||
$now
|
||||
);
|
||||
|
||||
$this->messageBus->dispatch($message);
|
||||
++$count;
|
||||
}
|
||||
|
||||
$this->logger->info('[DailyNotificationDigestCronjob] Dispatched daily digest messages', [
|
||||
'user_count' => $count,
|
||||
'last_execution' => $lastExecution->format('Y-m-d-H:i:s.u e'),
|
||||
'current_time' => $now->format('Y-m-d-H:i:s.u e'),
|
||||
]);
|
||||
|
||||
return [
|
||||
'last_execution' => $now->format('Y-m-d-H:i:s.u e'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,75 +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\Notification\Email\NotificationEmailHandlers;
|
||||
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
|
||||
use Chill\MainBundle\Notification\Email\NotificationMailer;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
readonly class ScheduleDailyNotificationDigestHandler
|
||||
{
|
||||
public function __construct(
|
||||
private NotificationRepository $notificationRepository,
|
||||
private UserRepository $userRepository,
|
||||
private NotificationMailer $notificationMailer,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function __invoke(ScheduleDailyNotificationDigestMessage $message): void
|
||||
{
|
||||
$userId = $message->getUserId();
|
||||
$lastExecutionDate = $message->getLastExecutionDateTime();
|
||||
$currentDate = $message->getCurrentDateTime();
|
||||
|
||||
$user = $this->userRepository->find($userId);
|
||||
if (null === $user) {
|
||||
$this->logger->warning('[ScheduleDailyNotificationDigestHandler] User not found', [
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
|
||||
throw new \InvalidArgumentException(sprintf('User with ID %s not found', $userId));
|
||||
}
|
||||
|
||||
// Get all notifications for this user between last execution and current date
|
||||
$notifications = $this->notificationRepository->findNotificationsForUserBetweenDates(
|
||||
$userId,
|
||||
$lastExecutionDate,
|
||||
$currentDate
|
||||
);
|
||||
|
||||
// Filter out notifications that should be sent in a daily digest
|
||||
$dailyNotifications = array_filter($notifications, fn ($notification) => $user->isNotificationDailyDigest($notification->getType()));
|
||||
|
||||
if ([] === $dailyNotifications) {
|
||||
$this->logger->info('[ScheduleDailyNotificationDigestHandler] No daily notifications found for user', [
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->notificationMailer->sendDailyDigest($user, $dailyNotifications);
|
||||
|
||||
$this->logger->info('[ScheduleDailyNotificationDigestHandler] Sent daily digest', [
|
||||
'user_id' => $userId,
|
||||
'notification_count' => count($dailyNotifications),
|
||||
]);
|
||||
}
|
||||
}
|
@@ -1,68 +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\Notification\Email\NotificationEmailHandlers;
|
||||
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
|
||||
use Chill\MainBundle\Notification\Email\NotificationMailer;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
readonly class SendImmediateNotificationEmailHandler
|
||||
{
|
||||
public function __construct(
|
||||
private NotificationRepository $notificationRepository,
|
||||
private UserRepository $userRepository,
|
||||
private NotificationMailer $notificationMailer,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __invoke(SendImmediateNotificationEmailMessage $message): void
|
||||
{
|
||||
$notification = $this->notificationRepository->find($message->getNotificationId());
|
||||
$addressee = $this->userRepository->find($message->getAddresseeId());
|
||||
|
||||
if (null === $notification) {
|
||||
$this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [
|
||||
'notification_id' => $message->getNotificationId(),
|
||||
]);
|
||||
|
||||
throw new \InvalidArgumentException(sprintf('Notification with ID %s not found', $message->getNotificationId()));
|
||||
}
|
||||
|
||||
if (null === $addressee) {
|
||||
$this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [
|
||||
'addressee_id' => $message->getAddresseeId(),
|
||||
]);
|
||||
|
||||
throw new \InvalidArgumentException(sprintf('User with ID %s not found', $message->getAddresseeId()));
|
||||
}
|
||||
|
||||
try {
|
||||
$this->notificationMailer->sendEmailToAddressee($notification, $addressee);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [
|
||||
'notification_id' => $message->getNotificationId(),
|
||||
'addressee_id' => $message->getAddresseeId(),
|
||||
'stacktrace' => $e->getTraceAsString(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,36 +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\Notification\Email\NotificationEmailMessages;
|
||||
|
||||
readonly class ScheduleDailyNotificationDigestMessage
|
||||
{
|
||||
public function __construct(
|
||||
private int $userId,
|
||||
private \DateTimeInterface $lastExecutionDate,
|
||||
private \DateTimeInterface $currentDate,
|
||||
) {}
|
||||
|
||||
public function getUserId(): int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getLastExecutionDateTime(): \DateTimeInterface
|
||||
{
|
||||
return $this->lastExecutionDate;
|
||||
}
|
||||
|
||||
public function getCurrentDateTime(): \DateTimeInterface
|
||||
{
|
||||
return $this->currentDate;
|
||||
}
|
||||
}
|
@@ -1,30 +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\Notification\Email\NotificationEmailMessages;
|
||||
|
||||
readonly class SendImmediateNotificationEmailMessage
|
||||
{
|
||||
public function __construct(
|
||||
private int $notificationId,
|
||||
private int $addresseeId,
|
||||
) {}
|
||||
|
||||
public function getNotificationId(): int
|
||||
{
|
||||
return $this->notificationId;
|
||||
}
|
||||
|
||||
public function getAddresseeId(): int
|
||||
{
|
||||
return $this->addresseeId;
|
||||
}
|
||||
}
|
@@ -13,32 +13,22 @@ namespace Chill\MainBundle\Notification\Email;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\NotificationComment;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
|
||||
use Doctrine\ORM\Event\PostPersistEventArgs;
|
||||
use Doctrine\ORM\Event\PostUpdateEventArgs;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
readonly class NotificationMailer
|
||||
class NotificationMailer
|
||||
{
|
||||
public function __construct(
|
||||
private MailerInterface $mailer,
|
||||
private LoggerInterface $logger,
|
||||
private MessageBusInterface $messageBus,
|
||||
private TranslatorInterface $translator,
|
||||
) {}
|
||||
public function __construct(private readonly MailerInterface $mailer, private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator) {}
|
||||
|
||||
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
|
||||
{
|
||||
$dests = [
|
||||
$comment->getNotification()->getSender(),
|
||||
...$comment->getNotification()->getAddressees()->toArray(),
|
||||
];
|
||||
$dests = [$comment->getNotification()->getSender(), ...$comment->getNotification()->getAddressees()->toArray()];
|
||||
|
||||
$uniqueDests = [];
|
||||
foreach ($dests as $dest) {
|
||||
@@ -79,67 +69,26 @@ readonly class NotificationMailer
|
||||
*/
|
||||
public function postPersistNotification(Notification $notification, PostPersistEventArgs $eventArgs): void
|
||||
{
|
||||
$this->sendNotificationEmailsToAddressees($notification);
|
||||
$this->sendNotificationEmailsToAddresses($notification);
|
||||
$this->sendNotificationEmailsToAddressesEmails($notification);
|
||||
}
|
||||
|
||||
private function sendNotificationEmailsToAddressees(Notification $notification): void
|
||||
public function postUpdateNotification(Notification $notification, PostUpdateEventArgs $eventArgs): void
|
||||
{
|
||||
if ('' === $notification->getType()) {
|
||||
$this->logger->warning('[NotificationMailer] Notification has no type, skipping email processing', [
|
||||
'notification_id' => $notification->getId(),
|
||||
]);
|
||||
|
||||
return;
|
||||
$this->sendNotificationEmailsToAddressesEmails($notification);
|
||||
}
|
||||
|
||||
foreach ($notification->getAllAddressees() as $addressee) {
|
||||
private function sendNotificationEmailsToAddresses(Notification $notification): void
|
||||
{
|
||||
foreach ($notification->getAddressees() as $addressee) {
|
||||
if (null === $addressee->getEmail()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->processNotificationForAddressee($notification, $addressee);
|
||||
}
|
||||
}
|
||||
|
||||
private function processNotificationForAddressee(Notification $notification, User $addressee): void
|
||||
{
|
||||
$notificationType = $notification->getType();
|
||||
|
||||
if ($addressee->isNotificationSendImmediately($notificationType)) {
|
||||
$this->scheduleImmediateEmail($notification, $addressee);
|
||||
}
|
||||
}
|
||||
|
||||
private function scheduleImmediateEmail(Notification $notification, User $addressee): void
|
||||
{
|
||||
$message = new SendImmediateNotificationEmailMessage(
|
||||
$notification->getId(),
|
||||
$addressee->getId()
|
||||
);
|
||||
|
||||
$this->messageBus->dispatch($message);
|
||||
|
||||
$this->logger->info('[NotificationMailer] Scheduled immediate email', [
|
||||
'notification_id' => $notification->getId(),
|
||||
'addressee_email' => $addressee->getEmail(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sends the email but is now called by the immediate notification email message handler.
|
||||
*
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function sendEmailToAddressee(Notification $notification, User $addressee): void
|
||||
{
|
||||
if (null === $addressee->getEmail()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($notification->isSystem()) {
|
||||
$email = new Email();
|
||||
$email->text($notification->getMessage());
|
||||
$email
|
||||
->text($notification->getMessage());
|
||||
} else {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
@@ -156,70 +105,19 @@ readonly class NotificationMailer
|
||||
|
||||
try {
|
||||
$this->mailer->send($email);
|
||||
$this->logger->info('[NotificationMailer] Email sent successfully', [
|
||||
'notification_id' => $notification->getId(),
|
||||
'addressee_email' => $addressee->getEmail(),
|
||||
]);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->warning('[NotificationMailer] Could not send an email notification', [
|
||||
$this->logger->warning('[NotificationMailer] could not send an email notification', [
|
||||
'to' => $addressee->getEmail(),
|
||||
'notification_id' => $notification->getId(),
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send daily digest email with multiple notifications to a user.
|
||||
*
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function sendDailyDigest(User $user, array $notifications): void
|
||||
{
|
||||
if (null === $user->getEmail() || [] === $notifications) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->htmlTemplate('@ChillMain/Notification/email_daily_digest.fr.md.twig')
|
||||
->context([
|
||||
'user' => $user,
|
||||
'notifications' => $notifications,
|
||||
'notification_count' => count($notifications),
|
||||
])
|
||||
->subject($this->translator->trans('notification.Daily Notification Digest'))
|
||||
->to($user->getEmail());
|
||||
|
||||
try {
|
||||
$this->mailer->send($email);
|
||||
$this->logger->info('[NotificationMailer] Daily digest email sent successfully', [
|
||||
'user_email' => $user->getEmail(),
|
||||
'notification_count' => count($notifications),
|
||||
]);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->warning('[NotificationMailer] Could not send daily digest email', [
|
||||
'to' => $user->getEmail(),
|
||||
'notification_count' => count($notifications),
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function sendNotificationEmailsToAddressesEmails(Notification $notification): void
|
||||
{
|
||||
foreach ($notification->getAddresseeUserGroups() as $userGroup) {
|
||||
|
||||
if (!$userGroup->hasEmail()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$emailAddress = $userGroup->getEmail();
|
||||
|
||||
foreach ($notification->getAddressesEmailsAdded() as $emailAddress) {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')
|
||||
|
@@ -1,30 +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\Notification\FlagProviders;
|
||||
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
class NotificationByUserFlagProvider implements NotificationFlagProviderInterface
|
||||
{
|
||||
public const FLAG = 'notif-by-user';
|
||||
|
||||
public function getFlag(): string
|
||||
{
|
||||
return self::FLAG;
|
||||
}
|
||||
|
||||
public function getLabel(): TranslatableInterface
|
||||
{
|
||||
return new TranslatableMessage('notification.flags.by-user');
|
||||
}
|
||||
}
|
@@ -1,21 +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\Notification\FlagProviders;
|
||||
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
interface NotificationFlagProviderInterface
|
||||
{
|
||||
public function getFlag(): string;
|
||||
|
||||
public function getLabel(): TranslatableInterface;
|
||||
}
|
@@ -1,30 +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\Notification\FlagProviders;
|
||||
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
class WorkflowTransitionNotificationFlagProvider implements NotificationFlagProviderInterface
|
||||
{
|
||||
public const FLAG = 'workflow-trans-notif';
|
||||
|
||||
public function getFlag(): string
|
||||
{
|
||||
return self::FLAG;
|
||||
}
|
||||
|
||||
public function getLabel(): TranslatableInterface
|
||||
{
|
||||
return new TranslatableMessage('notification.flags.workflow-trans');
|
||||
}
|
||||
}
|
@@ -1,33 +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\Notification;
|
||||
|
||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
|
||||
|
||||
final readonly class NotificationFlagManager
|
||||
{
|
||||
/**
|
||||
* @var array<NotificationFlagProviderInterface>
|
||||
*/
|
||||
private array $notificationFlagProviders;
|
||||
|
||||
public function __construct(
|
||||
iterable $notificationFlagProviders,
|
||||
) {
|
||||
$this->notificationFlagProviders = iterator_to_array($notificationFlagProviders);
|
||||
}
|
||||
|
||||
public function getAllNotificationFlagProviders(): array
|
||||
{
|
||||
return $this->notificationFlagProviders;
|
||||
}
|
||||
}
|
@@ -76,6 +76,24 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
||||
->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NumberParseException
|
||||
*/
|
||||
public function parse(string $phoneNumber): PhoneNumber
|
||||
{
|
||||
$sanitizedPhoneNumber = $phoneNumber;
|
||||
|
||||
if (str_starts_with($sanitizedPhoneNumber, '00')) {
|
||||
$sanitizedPhoneNumber = '+'.substr($sanitizedPhoneNumber, 2, null);
|
||||
}
|
||||
|
||||
if (!str_starts_with($sanitizedPhoneNumber, '+') && !str_starts_with($sanitizedPhoneNumber, '0')) {
|
||||
$sanitizedPhoneNumber = '+'.$sanitizedPhoneNumber;
|
||||
}
|
||||
|
||||
return $this->phoneNumberUtil->parse($sanitizedPhoneNumber, $this->config['default_carrier_code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type (mobile, landline, ...) for phone number.
|
||||
*/
|
||||
|
@@ -14,14 +14,13 @@ namespace Chill\MainBundle\Repository;
|
||||
use Chill\MainBundle\Entity\AddressReference;
|
||||
use Chill\MainBundle\Entity\PostalCode;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\NativeQuery;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
final readonly class AddressReferenceRepository implements AddressReferenceRepositoryInterface
|
||||
final readonly class AddressReferenceRepository implements ObjectRepository
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
@@ -66,121 +65,6 @@ final readonly class AddressReferenceRepository implements AddressReferenceRepos
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable
|
||||
{
|
||||
$terms = $this->buildTermsFromSearchString($search);
|
||||
if ([] === $terms) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$qb = $connection->createQueryBuilder();
|
||||
|
||||
$qb->select('row_number() OVER () AS row_number', 'var.street AS street', 'cmpc.id AS postcode_id', 'cmpc.code AS code', 'cmpc.label AS label', 'jsonb_object_agg(var.address_id, var.streetnumber ORDER BY var.row_number) AS positions')
|
||||
->from('view_chill_main_address_reference', 'var')
|
||||
->innerJoin('var', 'chill_main_postal_code', 'cmpc', 'cmpc.id = var.postcode_id')
|
||||
->groupBy('cmpc.id', 'var.street')
|
||||
->setFirstResult($firstResult)
|
||||
->setMaxResults($maxResults);
|
||||
|
||||
$paramId = 0;
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$qb->andWhere('var.address like UNACCENT(LOWER(?))');
|
||||
$qb->setParameter(++$paramId, "%{$term}%");
|
||||
}
|
||||
|
||||
if (null !== $postalCode) {
|
||||
$qb->andWhere('var.postcode_id = ?');
|
||||
$qb->setParameter(++$paramId, $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode);
|
||||
}
|
||||
|
||||
$result = $qb->executeQuery();
|
||||
|
||||
foreach ($result->iterateAssociative() as $row) {
|
||||
yield [...$row, 'positions' => json_decode($row['positions'], true, 512, JSON_THROW_ON_ERROR)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<AddressReference>
|
||||
*/
|
||||
public function findBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable
|
||||
{
|
||||
$terms = $this->buildTermsFromSearchString($search);
|
||||
if ([] === $terms) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rsm = new ResultSetMappingBuilder($this->entityManager);
|
||||
$rsm->addRootEntityFromClassMetadata(AddressReference::class, 'ar');
|
||||
$baseSql = 'SELECT '.$rsm->generateSelectClause(['ar' => 'ar']).' FROM chill_main_address_reference ar JOIN
|
||||
view_chill_main_address_reference var ON var.address_id = ar.id';
|
||||
$nql = $this->buildQueryBySearchString($rsm, $baseSql, $terms, $postalCode);
|
||||
|
||||
$orderBy = [];
|
||||
$pertinence = [];
|
||||
foreach ($terms as $k => $term) {
|
||||
$pertinence[] =
|
||||
"(EXISTS (SELECT 1 FROM unnest(string_to_array(address, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(:order{$k})))))::int";
|
||||
$pertinence[] = "(address LIKE UNACCENT(LOWER(:order{$k})))::int";
|
||||
$nql->setParameter('order'.$k, $term);
|
||||
}
|
||||
$orderBy[] = implode(' + ', $pertinence).' ASC';
|
||||
$orderBy[] = implode('row_number ASC', $orderBy);
|
||||
|
||||
$nql->setSQL($nql->getSQL().' ORDER BY '.implode(', ', $orderBy));
|
||||
|
||||
return $nql->toIterable();
|
||||
}
|
||||
|
||||
public function countBySearchString(string $search, PostalCode|int|null $postalCode = null): int
|
||||
{
|
||||
$terms = $this->buildTermsFromSearchString($search);
|
||||
if ([] === $terms) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$rsm = new ResultSetMappingBuilder($this->entityManager);
|
||||
$rsm->addScalarResult('c', 'c', Types::INTEGER);
|
||||
$nql = $this->buildQueryBySearchString($rsm, 'SELECT COUNT(var.*) AS c FROM view_chill_main_address_reference var', $terms, $postalCode);
|
||||
|
||||
return $nql->getSingleScalarResult();
|
||||
}
|
||||
|
||||
private function buildTermsFromSearchString(string $search): array
|
||||
{
|
||||
return array_filter(
|
||||
array_map(
|
||||
static fn (string $term) => trim($term),
|
||||
explode(' ', $search)
|
||||
),
|
||||
static fn (string $term) => '' !== $term
|
||||
);
|
||||
}
|
||||
|
||||
private function buildQueryBySearchString(ResultSetMapping $rsm, string $select, array $terms, PostalCode|int|null $postalCode = null): NativeQuery
|
||||
{
|
||||
$nql = $this->entityManager->createNativeQuery('', $rsm);
|
||||
|
||||
$sql = $select.' WHERE ';
|
||||
|
||||
$wheres = [];
|
||||
foreach ($terms as $k => $term) {
|
||||
$wheres[] = "var.address like :w{$k}";
|
||||
$nql->setParameter("w{$k}", '%'.$term.'%', Types::STRING);
|
||||
}
|
||||
|
||||
if (null !== $postalCode) {
|
||||
$wheres[] = 'var.postcode_id = :postalCode';
|
||||
$nql->setParameter('postalCode', $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode);
|
||||
}
|
||||
|
||||
$nql->setSQL($sql.implode(' AND ', $wheres));
|
||||
|
||||
return $nql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed|null $limit
|
||||
* @param mixed|null $offset
|
||||
|
@@ -1,20 +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\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\PostalCode;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
interface AddressReferenceRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable;
|
||||
}
|
@@ -290,19 +290,12 @@ final class NotificationRepository implements ObjectRepository
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function queryByAddressee(User $addressee): QueryBuilder
|
||||
private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('n');
|
||||
|
||||
$qb
|
||||
->leftJoin('n.addresseeUserGroups', 'aug')
|
||||
->leftJoin('aug.users', 'ugu')
|
||||
->where(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isMemberOf(':addressee', 'n.addressees'),
|
||||
$qb->expr()->eq('ugu.id', ':addressee')
|
||||
)
|
||||
)
|
||||
->where($qb->expr()->isMemberOf(':addressee', 'n.addressees'))
|
||||
->setParameter('addressee', $addressee);
|
||||
|
||||
return $qb;
|
||||
@@ -400,30 +393,4 @@ final class NotificationRepository implements ObjectRepository
|
||||
|
||||
return $nq->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all notifications for a user that were created between two dates.
|
||||
*
|
||||
* @return array|Notification[]
|
||||
*/
|
||||
public function findNotificationsForUserBetweenDates(int $userId, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array
|
||||
{
|
||||
$rsm = new Query\ResultSetMappingBuilder($this->em);
|
||||
$rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
|
||||
|
||||
$sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '.
|
||||
'FROM chill_main_notification cmn '.
|
||||
'JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id '.
|
||||
'WHERE cmnau.user_id = :userId '.
|
||||
'AND cmn.date >= :startDate '.
|
||||
'AND cmn.date <= :endDate '.
|
||||
'ORDER BY cmn.date DESC';
|
||||
|
||||
$nq = $this->em->createNativeQuery($sql, $rsm)
|
||||
->setParameter('userId', $userId)
|
||||
->setParameter('startDate', $startDate, Types::DATETIME_MUTABLE)
|
||||
->setParameter('endDate', $endDate, Types::DATETIME_MUTABLE);
|
||||
|
||||
return $nq->getResult();
|
||||
}
|
||||
}
|
||||
|
@@ -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\MainBundle\Repository;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
final readonly class PostalCodeForAddressReferenceRepository implements PostalCodeForAddressReferenceRepositoryInterface
|
||||
{
|
||||
public function __construct(private Connection $connection) {}
|
||||
|
||||
public function findPostalCode(string $search, int $firstResult = 0, int $maxResults = 50): iterable
|
||||
{
|
||||
$terms = $this->buildTermsFromSearchString($search);
|
||||
if ([] === $terms) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
|
||||
$qb->from('chill_main_postal_code', 'cmpc')
|
||||
->join('cmpc', 'view_chill_main_address_reference', 'vcmar', 'vcmar.postcode_id = cmpc.id')
|
||||
->join('vcmar', 'country', 'country', condition: 'cmpc.country_id = country.id')
|
||||
->setFirstResult($firstResult)
|
||||
->setMaxResults($maxResults)
|
||||
;
|
||||
|
||||
$qb->select(
|
||||
'DISTINCT ON (cmpc.code, cmpc.label) cmpc.id AS postcode_id',
|
||||
'cmpc.code AS code',
|
||||
'cmpc.label AS label',
|
||||
'country.id AS country_id',
|
||||
'country.countrycode AS country_code',
|
||||
'country.name AS country_name'
|
||||
);
|
||||
|
||||
$paramId = 0;
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$qb->andWhere('vcmar.address like ?');
|
||||
$qb->setParameter(++$paramId, "%{$term}%");
|
||||
}
|
||||
|
||||
$result = $qb->executeQuery();
|
||||
|
||||
foreach ($result->iterateAssociative() as $row) {
|
||||
yield [...$row, 'country_name' => json_decode($row['country_name'], true, 512, JSON_THROW_ON_ERROR)];
|
||||
}
|
||||
}
|
||||
|
||||
private function buildTermsFromSearchString(string $search): array
|
||||
{
|
||||
return array_filter(
|
||||
array_map(
|
||||
static fn (string $term) => trim($term),
|
||||
explode(' ', $search)
|
||||
),
|
||||
static fn (string $term) => '' !== $term
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,20 +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\Repository;
|
||||
|
||||
/**
|
||||
* Search for postal code using optimized materialized view.
|
||||
*/
|
||||
interface PostalCodeForAddressReferenceRepositoryInterface
|
||||
{
|
||||
public function findPostalCode(string $search, int $firstResult = 0, int $maxResults = 50): iterable;
|
||||
}
|
@@ -56,9 +56,7 @@ export const ISOToDatetime = (str: string | null): Date | null => {
|
||||
[time, timezone] = times.split(times.charAt(8)),
|
||||
[hours, minutes, seconds] = time.split(":").map((s) => parseInt(s));
|
||||
if ("0000" === timezone) {
|
||||
return new Date(
|
||||
Date.UTC(year, month - 1, date, hours, minutes, seconds),
|
||||
);
|
||||
return new Date(Date.UTC(year, month - 1, date, hours, minutes, seconds));
|
||||
}
|
||||
|
||||
return new Date(year, month - 1, date, hours, minutes, seconds);
|
||||
@@ -154,9 +152,7 @@ export const intervalISOToDays = (str: string | null): number | null => {
|
||||
vstring = "";
|
||||
break;
|
||||
default:
|
||||
throw Error(
|
||||
"this character should not appears: " + str.charAt(i),
|
||||
);
|
||||
throw Error("this character should not appears: " + str.charAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -126,7 +126,8 @@ function loadDynamicPicker(element) {
|
||||
-1 ===
|
||||
this.suggested.findIndex(
|
||||
(e) => e.type === entity.type && e.id === entity.id,
|
||||
)
|
||||
) &&
|
||||
"me" !== entity
|
||||
) {
|
||||
this.suggested.push(entity);
|
||||
}
|
||||
|
@@ -11,6 +11,11 @@ export interface Civility {
|
||||
// TODO
|
||||
}
|
||||
|
||||
export interface Household {
|
||||
type: "household";
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
id: number;
|
||||
type: "user_job";
|
||||
@@ -74,7 +79,6 @@ export interface Postcode {
|
||||
name: string;
|
||||
code: string;
|
||||
center: Point;
|
||||
country: Country;
|
||||
}
|
||||
|
||||
export interface Point {
|
||||
@@ -90,28 +94,6 @@ export interface Country {
|
||||
|
||||
export type AddressRefStatus = "match" | "to_review" | "reviewed";
|
||||
|
||||
/**
|
||||
* An interface to create an address
|
||||
*/
|
||||
export interface AddressCreation {
|
||||
confidential: boolean;
|
||||
isNoAddress: boolean;
|
||||
street: string;
|
||||
streetNumber: string;
|
||||
postcode: Postcode;
|
||||
point: Point; // [number, number]; // [longitude, latitude]
|
||||
addressReference: AddressReference;
|
||||
validFrom: DateTime|null;
|
||||
floor: string;
|
||||
corridor: string;
|
||||
steps: string;
|
||||
flat: string;
|
||||
buildingName: string;
|
||||
distribution: string;
|
||||
extra: string;
|
||||
}
|
||||
|
||||
|
||||
export interface Address {
|
||||
type: "address";
|
||||
address_id: number;
|
||||
@@ -130,7 +112,7 @@ export interface Address {
|
||||
confidential: boolean;
|
||||
lines: string[];
|
||||
addressReference: AddressReference | null;
|
||||
validFrom: DateTime | null; // TODO there is no null for validFrom
|
||||
validFrom: DateTime;
|
||||
validTo: DateTime | null;
|
||||
point: Point | null;
|
||||
refStatus: AddressRefStatus;
|
||||
@@ -238,3 +220,61 @@ export interface ExportGeneration {
|
||||
export interface PrivateCommentEmbeddable {
|
||||
comments: Record<number, string>;
|
||||
}
|
||||
|
||||
// API Exception types
|
||||
export interface TransportExceptionInterface {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ValidationExceptionInterface
|
||||
extends TransportExceptionInterface {
|
||||
name: "ValidationException";
|
||||
error: object;
|
||||
violations: string[];
|
||||
titles: string[];
|
||||
propertyPaths: string[];
|
||||
}
|
||||
|
||||
export interface AccessExceptionInterface extends TransportExceptionInterface {
|
||||
name: "AccessException";
|
||||
violations: string[];
|
||||
}
|
||||
|
||||
export interface NotFoundExceptionInterface
|
||||
extends TransportExceptionInterface {
|
||||
name: "NotFoundException";
|
||||
}
|
||||
|
||||
export interface ServerExceptionInterface extends TransportExceptionInterface {
|
||||
name: "ServerException";
|
||||
message: string;
|
||||
code: number;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface ConflictHttpExceptionInterface
|
||||
extends TransportExceptionInterface {
|
||||
name: "ConflictHttpException";
|
||||
violations: string[];
|
||||
}
|
||||
|
||||
export type ApiException =
|
||||
| ValidationExceptionInterface
|
||||
| AccessExceptionInterface
|
||||
| NotFoundExceptionInterface
|
||||
| ServerExceptionInterface
|
||||
| ConflictHttpExceptionInterface;
|
||||
|
||||
export interface Modal {
|
||||
showModal: boolean;
|
||||
modalDialogClass: string;
|
||||
}
|
||||
|
||||
export interface Selected {
|
||||
result: UserGroupOrUser;
|
||||
}
|
||||
|
||||
export interface addNewEntities {
|
||||
selected: Selected[];
|
||||
modal: Modal;
|
||||
}
|
||||
|
@@ -144,10 +144,7 @@
|
||||
</template>
|
||||
<template #action>
|
||||
<li v-if="!this.context.edit && this.useDatePane">
|
||||
<button
|
||||
class="btn btn-update change-icon"
|
||||
@click="closeEditPane"
|
||||
>
|
||||
<button class="btn btn-update change-icon" @click="closeEditPane">
|
||||
{{ $t("nav.next") }}
|
||||
<i class="fa fa-fw fa-arrow-right" />
|
||||
</button>
|
||||
@@ -370,8 +367,7 @@ export default {
|
||||
getTextTitle() {
|
||||
if (
|
||||
typeof this.options.title !== "undefined" &&
|
||||
(this.options.title.edit !== null ||
|
||||
this.options.title.create !== null)
|
||||
(this.options.title.edit !== null || this.options.title.create !== null)
|
||||
) {
|
||||
return this.context.edit
|
||||
? this.options.title.edit
|
||||
@@ -489,10 +485,7 @@ export default {
|
||||
if (!this.context.edit) {
|
||||
this.context.edit = true;
|
||||
this.context.addressId = params.addressId;
|
||||
console.log(
|
||||
"context is now edit, with address",
|
||||
params.addressId,
|
||||
);
|
||||
console.log("context is now edit, with address", params.addressId);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -619,9 +612,7 @@ export default {
|
||||
? this.entity.address.confidential
|
||||
: false;
|
||||
this.entity.selected.isNoAddress =
|
||||
this.context.edit && this.entity.address.text === ""
|
||||
? true
|
||||
: false;
|
||||
this.context.edit && this.entity.address.text === "" ? true : false;
|
||||
|
||||
this.entity.selected.country = this.context.edit
|
||||
? this.entity.address.country
|
||||
@@ -716,8 +707,7 @@ export default {
|
||||
// add the address reference, if any
|
||||
if (this.entity.selected.address.addressReference !== undefined) {
|
||||
newAddress = Object.assign(newAddress, {
|
||||
addressReference:
|
||||
this.entity.selected.address.addressReference,
|
||||
addressReference: this.entity.selected.address.addressReference,
|
||||
});
|
||||
} else {
|
||||
newAddress = Object.assign(newAddress, {
|
||||
@@ -737,10 +727,7 @@ export default {
|
||||
});
|
||||
}
|
||||
if (this.validTo && null !== this.entity.selected.valid.to) {
|
||||
console.log(
|
||||
"add validTo in fetch body",
|
||||
this.entity.selected.valid.to,
|
||||
);
|
||||
console.log("add validTo in fetch body", this.entity.selected.valid.to);
|
||||
newAddress = Object.assign(newAddress, {
|
||||
validTo: {
|
||||
datetime: `${this.entity.selected.valid.to.toISOString().split("T")[0]}T00:00:00+0100`,
|
||||
@@ -752,10 +739,7 @@ export default {
|
||||
newPostcode = Object.assign(newPostcode, {
|
||||
country: { id: this.entity.selected.country.id },
|
||||
}); //TODO why not assign postcodeBody here = Object.assign(postcodeBody, {'origin': 3}); ?
|
||||
console.log(
|
||||
"writeNew postcode is true! newPostcode: ",
|
||||
newPostcode,
|
||||
);
|
||||
console.log("writeNew postcode is true! newPostcode: ", newPostcode);
|
||||
newAddress = Object.assign(newAddress, {
|
||||
newPostcode: newPostcode,
|
||||
});
|
||||
|
@@ -72,10 +72,7 @@ export default {
|
||||
this.entity.addressMap.zoom,
|
||||
);
|
||||
} else {
|
||||
this.map.setView(
|
||||
lonLatForLeaflet(this.addressPoint.coordinates),
|
||||
15,
|
||||
);
|
||||
this.map.setView(lonLatForLeaflet(this.addressPoint.coordinates), 15);
|
||||
}
|
||||
|
||||
this.map.scrollWheelZoom.disable();
|
||||
@@ -105,9 +102,7 @@ export default {
|
||||
},
|
||||
update() {
|
||||
if (this.marker && this.entity.addressMap.center) {
|
||||
this.marker.setLatLng(
|
||||
lonLatForLeaflet(this.entity.addressMap.center),
|
||||
);
|
||||
this.marker.setLatLng(lonLatForLeaflet(this.entity.addressMap.center));
|
||||
this.map.panTo(lonLatForLeaflet(this.entity.addressMap.center));
|
||||
}
|
||||
},
|
||||
|
@@ -76,9 +76,7 @@ export default {
|
||||
props: ["entity", "context", "updateMapCenter", "flag", "checkErrors"],
|
||||
data() {
|
||||
return {
|
||||
value: this.context.edit
|
||||
? this.entity.address.addressReference
|
||||
: null,
|
||||
value: this.context.edit ? this.entity.address.addressReference : null,
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
@@ -151,8 +149,7 @@ export default {
|
||||
.then(
|
||||
(addresses) =>
|
||||
new Promise((resolve, reject) => {
|
||||
this.entity.loaded.addresses =
|
||||
addresses.results;
|
||||
this.entity.loaded.addresses = addresses.results;
|
||||
this.isLoading = false;
|
||||
resolve();
|
||||
}),
|
||||
@@ -169,8 +166,7 @@ export default {
|
||||
.then(
|
||||
(addresses) =>
|
||||
new Promise((resolve, reject) => {
|
||||
this.entity.loaded.addresses =
|
||||
addresses.results;
|
||||
this.entity.loaded.addresses = addresses.results;
|
||||
this.isLoading = false;
|
||||
resolve();
|
||||
}),
|
||||
|
@@ -136,9 +136,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
transName(value) {
|
||||
return value.code && value.name
|
||||
? `${value.name} (${value.code})`
|
||||
: "";
|
||||
return value.code && value.name ? `${value.name} (${value.code})` : "";
|
||||
},
|
||||
selectCity(value) {
|
||||
console.log(value);
|
||||
@@ -146,8 +144,7 @@ export default {
|
||||
this.entity.selected.postcode.name = value.name;
|
||||
this.entity.selected.postcode.code = value.code;
|
||||
if (value.center) {
|
||||
this.entity.selected.postcode.coordinates =
|
||||
value.center.coordinates;
|
||||
this.entity.selected.postcode.coordinates = value.center.coordinates;
|
||||
}
|
||||
this.entity.selected.writeNew.postcode = false;
|
||||
this.$emit("getReferenceAddresses", value);
|
||||
@@ -168,8 +165,7 @@ export default {
|
||||
.then(
|
||||
(cities) =>
|
||||
new Promise((resolve, reject) => {
|
||||
this.entity.loaded.cities =
|
||||
cities.results.filter(
|
||||
this.entity.loaded.cities = cities.results.filter(
|
||||
(c) => c.origin !== 3,
|
||||
); // filter out user-defined cities
|
||||
this.isLoading = false;
|
||||
@@ -188,8 +184,7 @@ export default {
|
||||
.then(
|
||||
(cities) =>
|
||||
new Promise((resolve, reject) => {
|
||||
this.entity.loaded.cities =
|
||||
cities.results.filter(
|
||||
this.entity.loaded.cities = cities.results.filter(
|
||||
(c) => c.origin !== 3,
|
||||
); // filter out user-defined cities
|
||||
this.isLoading = false;
|
||||
|
@@ -42,12 +42,8 @@ export default {
|
||||
sortedCountries() {
|
||||
const countries = this.entity.loaded.countries;
|
||||
let sortedCountries = [];
|
||||
sortedCountries.push(
|
||||
...countries.filter((c) => c.countryCode === "FR"),
|
||||
);
|
||||
sortedCountries.push(
|
||||
...countries.filter((c) => c.countryCode === "BE"),
|
||||
);
|
||||
sortedCountries.push(...countries.filter((c) => c.countryCode === "FR"));
|
||||
sortedCountries.push(...countries.filter((c) => c.countryCode === "BE"));
|
||||
sortedCountries.push(
|
||||
...countries
|
||||
.filter((c) => c.countryCode !== "FR")
|
||||
|
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div v-if="insideModal === false" class="loading">
|
||||
<i
|
||||
v-if="flag.loading"
|
||||
class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
|
||||
/>
|
||||
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw" />
|
||||
<span class="sr-only">{{ $t("loading") }}</span>
|
||||
</div>
|
||||
|
||||
@@ -142,8 +139,7 @@ export default {
|
||||
address["street"] = this.entity.selected.address.street
|
||||
? this.entity.selected.address.street
|
||||
: null;
|
||||
address["streetNumber"] = this.entity.selected.address
|
||||
.streetNumber
|
||||
address["streetNumber"] = this.entity.selected.address.streetNumber
|
||||
? this.entity.selected.address.streetNumber
|
||||
: null;
|
||||
address["floor"] = this.entity.selected.address.floor
|
||||
@@ -158,12 +154,10 @@ export default {
|
||||
address["flat"] = this.entity.selected.address.flat
|
||||
? this.entity.selected.address.flat
|
||||
: null;
|
||||
address["buildingName"] = this.entity.selected.address
|
||||
.buildingName
|
||||
address["buildingName"] = this.entity.selected.address.buildingName
|
||||
? this.entity.selected.address.buildingName
|
||||
: null;
|
||||
address["distribution"] = this.entity.selected.address
|
||||
.distribution
|
||||
address["distribution"] = this.entity.selected.address.distribution
|
||||
? this.entity.selected.address.distribution
|
||||
: null;
|
||||
address["extra"] = this.entity.selected.address.extra
|
||||
|
@@ -2,10 +2,7 @@
|
||||
<div class="address-form">
|
||||
<!-- Not display in modal -->
|
||||
<div v-if="insideModal === false" class="loading">
|
||||
<i
|
||||
v-if="flag.loading"
|
||||
class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
|
||||
/>
|
||||
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw" />
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
|
||||
|
@@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<div v-if="!onlyButton" class="mt-4 flex-grow-1">
|
||||
<div class="loading">
|
||||
<i
|
||||
v-if="flag.loading"
|
||||
class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
|
||||
/>
|
||||
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw" />
|
||||
<span class="sr-only">{{ $t("loading") }}</span>
|
||||
</div>
|
||||
|
||||
@@ -45,9 +42,7 @@
|
||||
name="button"
|
||||
:title="$t(getTextButton)"
|
||||
>
|
||||
<span v-if="displayTextButton">{{
|
||||
$t(getTextButton)
|
||||
}}</span>
|
||||
<span v-if="displayTextButton">{{ $t(getTextButton) }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</action-buttons>
|
||||
@@ -60,9 +55,7 @@
|
||||
:use-date-pane="useDatePane"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="this.context.target.name === 'household' || this.context.edit"
|
||||
>
|
||||
<div v-if="this.context.target.name === 'household' || this.context.edit">
|
||||
<action-buttons :options="this.options" :defaultz="this.defaultz">
|
||||
<template #action>
|
||||
<button
|
||||
@@ -73,9 +66,7 @@
|
||||
name="button"
|
||||
:title="$t(getTextButton)"
|
||||
>
|
||||
<span v-if="displayTextButton">{{
|
||||
$t(getTextButton)
|
||||
}}</span>
|
||||
<span v-if="displayTextButton">{{ $t(getTextButton) }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</action-buttons>
|
||||
@@ -97,9 +88,7 @@
|
||||
name="button"
|
||||
:title="$t(getTextButton)"
|
||||
>
|
||||
<span v-if="displayTextButton">{{
|
||||
$t(getTextButton)
|
||||
}}</span>
|
||||
<span v-if="displayTextButton">{{ $t(getTextButton) }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</action-buttons>
|
||||
@@ -165,9 +154,7 @@ export default {
|
||||
: this.defaultz.button.text.create;
|
||||
},
|
||||
getSuccessText() {
|
||||
return this.context.edit
|
||||
? "address_edit_success"
|
||||
: "address_new_success";
|
||||
return this.context.edit ? "address_edit_success" : "address_new_success";
|
||||
},
|
||||
onlyButton() {
|
||||
return typeof this.options.onlyButton !== "undefined"
|
||||
|
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div v-if="insideModal === false" class="loading">
|
||||
<i
|
||||
v-if="flag.loading"
|
||||
class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
|
||||
/>
|
||||
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw" />
|
||||
<span class="sr-only">{{ $t("loading") }}</span>
|
||||
</div>
|
||||
|
||||
|
@@ -1,33 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import AddressPicker from "ChillMainAssets/vuejs/AddressPicker/AddressPicker.vue";
|
||||
import {Ref, ref} from "vue";
|
||||
|
||||
const showModal: Ref<boolean> = ref(false);
|
||||
|
||||
const modalDialogClasses = {"modal-dialog": true, "modal-dialog-scrollable": true, "modal-xl": true};
|
||||
|
||||
const clickButton = () => {
|
||||
showModal.value = true;
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<modal v-if="showModal" :hide-footer="false" :modal-dialog-class="modalDialogClasses" @close="closeModal">
|
||||
<template v-slot:header>TODO</template>
|
||||
<template v-slot:body>
|
||||
<AddressPicker></AddressPicker>
|
||||
</template>
|
||||
</modal>
|
||||
<button class="btn btn-submit" type="button" @click="clickButton">SEARCH ADDRESS</button>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -1,142 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {Address, AddressReference} from "ChillMainAssets/types";
|
||||
import SearchBar from "ChillMainAssets/vuejs/AddressPicker/Component/SearchBar.vue";
|
||||
import {
|
||||
AddressAggregated,
|
||||
AssociatedPostalCode, fetchAddressReference,
|
||||
getAddressesAggregated,
|
||||
getPostalCodes,
|
||||
} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
||||
import {computed, Ref, ref} from "vue";
|
||||
import AddressAggregatedList from "ChillMainAssets/vuejs/AddressPicker/Component/AddressAggregatedList.vue";
|
||||
import AddressDetailsForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressDetailsForm.vue";
|
||||
|
||||
interface AddressPickerProps {
|
||||
suggestions?: Address[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<AddressPickerProps>(), {
|
||||
suggestions: () => [],
|
||||
});
|
||||
|
||||
const addresses: Ref<AddressAggregated[]> = ref([]);
|
||||
const postalCodes: Ref<AssociatedPostalCode[]> = ref([]);
|
||||
const searchTokens: Ref<string[]> = ref([]);
|
||||
const addressReference: Ref<AddressReference|null> = ref(null);
|
||||
|
||||
let abortControllerSearchAddress: null | AbortController = null;
|
||||
let abortControllerSearchPostalCode: null | AbortController = null;
|
||||
|
||||
const searchResultsClasses = computed(() => ({
|
||||
"mid-size": addressReference !== null,
|
||||
}));
|
||||
|
||||
|
||||
const onSearch = async function (search: string): Promise<void> {
|
||||
performSearchForAddress(search);
|
||||
performSearchForPostalCode(search);
|
||||
searchTokens.value = [search];
|
||||
};
|
||||
|
||||
const onPickPosition = async (id: string) => {
|
||||
console.log('Pick Position', id);
|
||||
addressReference.value = await fetchAddressReference(id);
|
||||
}
|
||||
|
||||
const performSearchForAddress = async (search: string): Promise<void> => {
|
||||
if (null !== abortControllerSearchAddress) {
|
||||
abortControllerSearchAddress.abort();
|
||||
}
|
||||
|
||||
if ("" === search) {
|
||||
addresses.value = [];
|
||||
abortControllerSearchAddress = null;
|
||||
return;
|
||||
}
|
||||
|
||||
abortControllerSearchAddress = new AbortController();
|
||||
|
||||
console.log("onSearch", search);
|
||||
try {
|
||||
addresses.value = await getAddressesAggregated(
|
||||
search,
|
||||
abortControllerSearchAddress,
|
||||
);
|
||||
abortControllerSearchAddress = null;
|
||||
|
||||
// check if there is only one result
|
||||
if (addresses.value.length === 1 && Object.keys(addresses.value[0].positions).length === 1) {
|
||||
onPickPosition(Object.keys(addresses.value[0].positions)[0]);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof DOMException && e.name === "AbortError") {
|
||||
console.log("search aborted for:", search);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const performSearchForPostalCode = async (search: string): Promise<void> => {
|
||||
if (null !== abortControllerSearchPostalCode) {
|
||||
abortControllerSearchPostalCode.abort();
|
||||
}
|
||||
|
||||
if ("" === search) {
|
||||
addresses.value = [];
|
||||
abortControllerSearchPostalCode = null;
|
||||
return;
|
||||
}
|
||||
|
||||
abortControllerSearchPostalCode = new AbortController();
|
||||
|
||||
console.log("onSearch", search);
|
||||
try {
|
||||
postalCodes.value = await getPostalCodes(
|
||||
search,
|
||||
abortControllerSearchPostalCode,
|
||||
);
|
||||
abortControllerSearchPostalCode = null;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof DOMException && e.name === "AbortError") {
|
||||
console.log("search postal code aborted for:", search);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<search-bar @search="onSearch"></search-bar>
|
||||
<div class="address-pick-content">
|
||||
<div class="search-results" :class="searchResultsClasses">
|
||||
<address-aggregated-list :addresses="addresses" :search-tokens="searchTokens" @pick-position="(id) => onPickPosition(id)"></address-aggregated-list>
|
||||
</div>
|
||||
<div v-if="addressReference !== null" class="address-details-form">
|
||||
<address-details-form :address="addressReference"></address-details-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.address-pick-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
.search-results {
|
||||
&.mid-size {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
.address-details-form {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,25 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {AddressAggregated} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
||||
import AddressAggregatedListItem from "ChillMainAssets/vuejs/AddressPicker/Component/AddressAggregatedListItem.vue";
|
||||
|
||||
interface AddressAggregatedListProps {
|
||||
addresses: AddressAggregated[];
|
||||
searchTokens: string[];
|
||||
}
|
||||
|
||||
const props = defineProps<AddressAggregatedListProps>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
pickPosition: [id: string]
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-for="a in props.addresses" :key="a.row_number">
|
||||
<address-aggregated-list-item :address="a" :search-tokens="props.searchTokens" @pick-position="(id) => emit('pickPosition', id)"></address-aggregated-list-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -1,82 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {AddressAggregated} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
||||
import {computed, ref} from "vue";
|
||||
|
||||
interface AddressAggregatedListItemProps {
|
||||
address: AddressAggregated;
|
||||
searchTokens: string[];
|
||||
}
|
||||
|
||||
const props = defineProps<AddressAggregatedListItemProps>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
pickPosition: [id: string]
|
||||
}>();
|
||||
|
||||
const showAllPositions = ref<boolean>(false);
|
||||
const positionsToShow = computed((): Record<string, string> => {
|
||||
const obj: Record<string, any> = {};
|
||||
let count = 0;
|
||||
for (const [id, position] of Object.entries(props.address.positions)) {
|
||||
obj[id] = position;
|
||||
count++;
|
||||
if (count >= 10 && !showAllPositions.value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
})
|
||||
const needToShowMorePosition = computed(() => {
|
||||
return Object.keys(props.address.positions).length > 10;
|
||||
})
|
||||
|
||||
const onClickButton = (id: string) => {
|
||||
console.log('onClickButton', id);
|
||||
emit('pickPosition', id);
|
||||
}
|
||||
const displayAllPositions = () => {
|
||||
showAllPositions.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="street">
|
||||
<span>{{ props.address.street }}</span>
|
||||
</div>
|
||||
<div class="postcode">
|
||||
<span>{{ props.address.code }}</span> <span>{{ address.label }}</span>
|
||||
</div>
|
||||
<div class="positions">
|
||||
<ul>
|
||||
<li v-for="(position, id) in positionsToShow" :key="id" >
|
||||
<button type="button" @click="onClickButton(id)" >
|
||||
{{ position }}
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="needToShowMorePosition">
|
||||
<button @click="displayAllPositions">show all</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.street {
|
||||
font-variant: small-caps;
|
||||
font-weight: bold;
|
||||
}
|
||||
.postcode {
|
||||
font-variant: small-caps;
|
||||
}
|
||||
.positions ul {
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,27 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {AddressReference} from "ChillMainAssets/types";
|
||||
import {computed} from "vue";
|
||||
import {addressReferenceToAddress} from "ChillMainAssets/vuejs/AddressPicker/helper";
|
||||
import AddressDetailsContent from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsContent.vue";
|
||||
|
||||
export interface AddressDetailsFormProps {
|
||||
address: AddressReference;
|
||||
}
|
||||
|
||||
const props = defineProps<AddressDetailsFormProps>();
|
||||
|
||||
const address = computed(() => addressReferenceToAddress(props.address));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
FORM
|
||||
</div>
|
||||
<div>
|
||||
<address-details-content :address="address"></address-details-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -1,39 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ADDRESS_PICKER_SEARCH_FOR_ADDRESSES, trans } from 'translator';
|
||||
const emits = defineEmits<{
|
||||
search: [search: string];
|
||||
}>();
|
||||
|
||||
let searchTimer = 0;
|
||||
let searchString: string;
|
||||
|
||||
const onInput = function (event: InputEvent) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const value = target.value;
|
||||
searchString = value;
|
||||
|
||||
if (0 === searchTimer) {
|
||||
window.clearTimeout(searchTimer);
|
||||
searchTimer = 0;
|
||||
}
|
||||
|
||||
searchTimer = window.setTimeout(() => {
|
||||
if (value === searchString) {
|
||||
emits("search", value);
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
|
||||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="search" class="form-control" @input="onInput" :placeholder="trans(ADDRESS_PICKER_SEARCH_FOR_ADDRESSES)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@@ -1,69 +0,0 @@
|
||||
import {AddressReference, TranslatableString} from "ChillMainAssets/types";
|
||||
|
||||
export interface AddressAggregated {
|
||||
row_number: number;
|
||||
street: string;
|
||||
postcode_id: number;
|
||||
code: string;
|
||||
label: string;
|
||||
positions: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface AssociatedPostalCode {
|
||||
postcode_id: number;
|
||||
code: string;
|
||||
label: string;
|
||||
country_id: number;
|
||||
country_code: string;
|
||||
country_name: TranslatableString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws {DOMException} when fetch is aborted, the property name is always equals to 'AbortError'
|
||||
*/
|
||||
export const getAddressesAggregated = async (
|
||||
search: string,
|
||||
abortController: AbortController,
|
||||
): Promise<AddressAggregated[]> => {
|
||||
const params = new URLSearchParams({ q: search.trim() });
|
||||
let response = null;
|
||||
|
||||
response = await fetch(
|
||||
`/api/1.0/main/address-reference/aggregated/search?${params}`,
|
||||
{ signal: abortController.signal },
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
throw new Error(response.statusText);
|
||||
};
|
||||
|
||||
export const getPostalCodes = async (
|
||||
search: string,
|
||||
abortController: AbortController,
|
||||
): Promise<AssociatedPostalCode[]> => {
|
||||
const params = new URLSearchParams({ q: search.trim() });
|
||||
let response = null;
|
||||
|
||||
response = await fetch(
|
||||
`/api/1.0/main/address-reference/postal-code/search?${params}`,
|
||||
{ signal: abortController.signal },
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
throw new Error(response.statusText);
|
||||
};
|
||||
|
||||
export const fetchAddressReference = async (id: string): Promise<AddressReference> => {
|
||||
const response = await fetch(`/api/1.0/main/address-reference/${id}.json`);
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
throw new Error(response.statusText);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user