Compare commits
202 Commits
main
...
dev/portai
| Author | SHA1 | Date |
|---|---|---|
|
|
c673fcd83b | |
|
|
f0ae5a8c8a | |
|
|
f6a2159177 | |
|
|
9513067ac5 | |
|
|
52b8ba0a10 | |
|
|
70842d6fe9 | |
|
|
7da97b0d02 | |
|
|
f09dd20d2b | |
|
|
94c0fd7c42 | |
|
|
9d541410c4 | |
|
|
a24849abe3 | |
|
|
83c4c221af | |
|
|
9d394c34f4 | |
|
|
04b8b26d65 | |
|
|
82c7c15068 | |
|
|
9d37a7c549 | |
|
|
d884ff4155 | |
|
|
7e08998005 | |
|
|
8c9a5da604 | |
|
|
f2123e911d | |
|
|
9dc97c5843 | |
|
|
3744d81035 | |
|
|
a6fdb59521 | |
|
|
f6ce0e6229 | |
|
|
7db986468c | |
|
|
53c3180d33 | |
|
|
8193e339b0 | |
|
|
346a05e51d | |
|
|
75e5921be1 | |
|
|
2d84ee8ec4 | |
|
|
cb7afab382 | |
|
|
00ed7ef491 | |
|
|
b974b56a17 | |
|
|
5f50584f0d | |
|
|
6aacf0cefc | |
|
|
e6068fd538 | |
|
|
a219f0f067 | |
|
|
ec3fc7f5ca | |
|
|
aee352924e | |
|
|
83da6d0be4 | |
|
|
afac1467fa | |
|
|
519556d35e | |
|
|
e4f63c9b85 | |
|
|
772b920a44 | |
|
|
c54df8a327 | |
|
|
2418e43703 | |
|
|
36fe5f5588 | |
|
|
003ee40992 | |
|
|
b430e13e3b | |
|
|
2d7adf20ec | |
|
|
5a39804dd4 | |
|
|
9a51c2d86f | |
|
|
3680621fcc | |
|
|
e1659accab | |
|
|
016c415c11 | |
|
|
2b9b030d9a | |
|
|
bb959a1ac1 | |
|
|
143277455a | |
|
|
0fc507d4c7 | |
|
|
8c7336b821 | |
|
|
9270849e12 | |
|
|
abbaf016cc | |
|
|
00c58b55d1 | |
|
|
e818a17371 | |
|
|
6e6d02e658 | |
|
|
0222274a17 | |
|
|
ead3666a4f | |
|
|
25bad81f03 | |
|
|
fd02fc26f1 | |
|
|
6dc6d3bfa9 | |
|
|
20509385f6 | |
|
|
3485bcc48f | |
|
|
1a49265658 | |
|
|
a01df6345a | |
|
|
41c6e82a13 | |
|
|
307e615fb3 | |
|
|
20bc6e92bc | |
|
|
1788ec9062 | |
|
|
3ef774d7e0 | |
|
|
dc5eb702a3 | |
|
|
346d89f42e | |
|
|
ec29f42f90 | |
|
|
c75eda74a3 | |
|
|
633c255598 | |
|
|
5e52386233 | |
|
|
1008d636a6 | |
|
|
3ca5eea877 | |
|
|
0bcab27a1d | |
|
|
eaff14acc6 | |
|
|
889010b5ad | |
|
|
a540bb5d9e | |
|
|
9257709605 | |
|
|
1516e8c890 | |
|
|
6964bc0214 | |
|
|
febd2ad6b2 | |
|
|
e33b0b8248 | |
|
|
8d095a368f | |
|
|
5a7be977ba | |
|
|
1e33782f75 | |
|
|
245f044a40 | |
|
|
446f585cc9 | |
|
|
218923dfb7 | |
|
|
84e5d7c87a | |
|
|
f52ad375b4 | |
|
|
7b7f58363a | |
|
|
52f3d2a3de | |
|
|
3d832b4280 | |
|
|
9dd820d47f | |
|
|
3b1a3dee9a | |
|
|
71c6f82b77 | |
|
|
26637e497a | |
|
|
3ca1446b91 | |
|
|
8a19b01893 | |
|
|
2dc5710e06 | |
|
|
92dd3a3d23 | |
|
|
2d9b44ddb6 | |
|
|
9dc79eaa7d | |
|
|
716bfb8ce1 | |
|
|
cc93387154 | |
|
|
61e43dcd98 | |
|
|
0d498d4570 | |
|
|
328a89f11f | |
|
|
ccd44e3560 | |
|
|
9da1edaa92 | |
|
|
3f55eefddc | |
|
|
b81b168ec3 | |
|
|
3894d72439 | |
|
|
2f3e28757e | |
|
|
1f7d844d6f | |
|
|
c757a841c5 | |
|
|
790f77c430 | |
|
|
f9c63d6753 | |
|
|
95f806efce | |
|
|
37ba0a5e6a | |
|
|
371c511ecf | |
|
|
993188ac4f | |
|
|
18dc5f8492 | |
|
|
f2166b604e | |
|
|
8d92d3f9fc | |
|
|
5ceed1f2f2 | |
|
|
450543fab7 | |
|
|
d543e69863 | |
|
|
7021b28163 | |
|
|
c55e9fa039 | |
|
|
bdf9f0478e | |
|
|
1053a2ab22 | |
|
|
6efbeb0fa2 | |
|
|
1ee9a0110b | |
|
|
cbdb47fb17 | |
|
|
e6c8d5a462 | |
|
|
7e272b2b2f | |
|
|
6670fbc8b8 | |
|
|
1e8d5e1eaf | |
|
|
2e99457e16 | |
|
|
cde6c529a9 | |
|
|
a3f993b858 | |
|
|
d2c20b9423 | |
|
|
89ed7049b9 | |
|
|
16dd919a5d | |
|
|
301f7bb445 | |
|
|
05d8ca0499 | |
|
|
e17e8e0eb2 | |
|
|
e6391279fe | |
|
|
cf16ec09a1 | |
|
|
943752a002 | |
|
|
a7e7298310 | |
|
|
3337b8c001 | |
|
|
6446eb2ce1 | |
|
|
a10b499522 | |
|
|
e77e92d39f | |
|
|
00e3003257 | |
|
|
f1b953d005 | |
|
|
4aadaa351a | |
|
|
fcb69f987f | |
|
|
d7677db885 | |
|
|
8eb5cf433d | |
|
|
fed351b433 | |
|
|
0a602fb52e | |
|
|
e87fdd32e4 | |
|
|
cfe89f58db | |
|
|
1d2debf364 | |
|
|
3271da59fa | |
|
|
d8df0bc1f4 | |
|
|
c3d3218bff | |
|
|
f24fb0180d | |
|
|
e360019e58 | |
|
|
8e38fe47db | |
|
|
b54fe41795 | |
|
|
798ca4ba07 | |
|
|
d43b516826 | |
|
|
c99b575814 | |
|
|
65ff838dd9 | |
|
|
4a2f9d9547 | |
|
|
10a8eb2255 | |
|
|
8f232498b8 | |
|
|
bbe50dbfd9 | |
|
|
436284c9c7 | |
|
|
228ef8cbe9 | |
|
|
c81142e4a5 | |
|
|
6a9b7568af | |
|
|
57e115a6a8 | |
|
|
79c5596766 |
5
.env
|
|
@ -62,3 +62,8 @@ MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure
|
||||||
# The secret used to sign the JWTs
|
# The secret used to sign the JWTs
|
||||||
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
|
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
|
||||||
###< symfony/mercure-bundle ###
|
###< symfony/mercure-bundle ###
|
||||||
|
|
||||||
|
###> aws/aws-sdk-php-symfony ###
|
||||||
|
AWS_KEY=not-a-real-key
|
||||||
|
AWS_SECRET=@@not-a-real-secret
|
||||||
|
###< aws/aws-sdk-php-symfony ###
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
/.env.*.local
|
/.env.*.local
|
||||||
/config/secrets/prod/prod.decrypt.private.php
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
/public/bundles/
|
/public/bundles/
|
||||||
|
/public/uploads/
|
||||||
/var/
|
/var/
|
||||||
/vendor/
|
/vendor/
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
@ -23,3 +24,7 @@
|
||||||
/public/assets/
|
/public/assets/
|
||||||
/assets/vendor/
|
/assets/vendor/
|
||||||
###< symfony/asset-mapper ###
|
###< symfony/asset-mapper ###
|
||||||
|
|
||||||
|
###> IntelliJ IDEA ###
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,20 @@
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure-bundle" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure-bundle" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/firebase/php-jwt" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/firebase/php-jwt" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/aws/aws-crt-php" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/aws/aws-sdk-php" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/aws/aws-sdk-php-symfony" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/knplabs/knp-time-bundle" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/mtdowling/jmespath.php" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="quill.snow" level="application" />
|
||||||
|
<orderEntry type="library" name="quill" level="application" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
|
|
@ -10,6 +10,11 @@
|
||||||
<option name="highlightLevel" value="WARNING" />
|
<option name="highlightLevel" value="WARNING" />
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="PhpCodeSniffer">
|
||||||
|
<phpcs_settings>
|
||||||
|
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="30000" />
|
||||||
|
</phpcs_settings>
|
||||||
|
</component>
|
||||||
<component name="PhpIncludePathManager">
|
<component name="PhpIncludePathManager">
|
||||||
<include_path>
|
<include_path>
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
||||||
|
|
@ -83,7 +88,6 @@
|
||||||
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
||||||
<path value="$PROJECT_DIR$/vendor/lcobucci/clock" />
|
<path value="$PROJECT_DIR$/vendor/lcobucci/clock" />
|
||||||
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
||||||
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/composer" />
|
<path value="$PROJECT_DIR$/vendor/composer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
||||||
|
|
@ -141,7 +145,6 @@
|
||||||
<path value="$PROJECT_DIR$/vendor/defuse/php-encryption" />
|
<path value="$PROJECT_DIR$/vendor/defuse/php-encryption" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" />
|
<path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/stimulus-bundle" />
|
<path value="$PROJECT_DIR$/vendor/symfony/stimulus-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
||||||
|
|
@ -164,9 +167,26 @@
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/mercure" />
|
<path value="$PROJECT_DIR$/vendor/symfony/mercure" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/mercure-bundle" />
|
<path value="$PROJECT_DIR$/vendor/symfony/mercure-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/firebase/php-jwt" />
|
<path value="$PROJECT_DIR$/vendor/firebase/php-jwt" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/knplabs/knp-time-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/aws/aws-sdk-php" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/aws/aws-crt-php" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/aws/aws-sdk-php-symfony" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/mtdowling/jmespath.php" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
||||||
|
<component name="PhpStan">
|
||||||
|
<PhpStan_settings>
|
||||||
|
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="60000" />
|
||||||
|
</PhpStan_settings>
|
||||||
|
</component>
|
||||||
<component name="PhpStanOptionsConfiguration">
|
<component name="PhpStanOptionsConfiguration">
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
|
|
@ -175,6 +195,11 @@
|
||||||
<PhpUnitSettings configuration_file_path="$PROJECT_DIR$/phpunit.xml.dist" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" />
|
<PhpUnitSettings configuration_file_path="$PROJECT_DIR$/phpunit.xml.dist" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" />
|
||||||
</phpunit_settings>
|
</phpunit_settings>
|
||||||
</component>
|
</component>
|
||||||
|
<component name="Psalm">
|
||||||
|
<Psalm_settings>
|
||||||
|
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="60000" />
|
||||||
|
</Psalm_settings>
|
||||||
|
</component>
|
||||||
<component name="PsalmOptionsConfiguration">
|
<component name="PsalmOptionsConfiguration">
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
## Notes
|
||||||
|
- Certaines abbreviations sont utilisées afin de simplifier le code et d'éviter les répétitions ou noms trop longs :
|
||||||
|
- `uo` pour `User Organization`
|
||||||
|
- `uoId` pour `User Organization Id`
|
||||||
|
- `oa` pour `Organization Application`
|
||||||
|
- `at` pour `Access Token`
|
||||||
|
- A delete command is available to delete roles
|
||||||
|
|
||||||
|
|
||||||
|
### ROLES
|
||||||
|
```bash
|
||||||
|
php bin/console app:delete-role ROLE_NAME
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tabulator
|
||||||
|
- Certaines fonctions sont déjà disponibles (snippet) mais commentées, car on ne les utilise pas
|
||||||
|
- Exemples de sorting et filtering sont disponibles dans 'src/controller/organization.php' L.268
|
||||||
|
|
||||||
|
### Frontend:
|
||||||
|
- Le Body/content de chaque page sont dans des div avec le style suivant :
|
||||||
|
``` html
|
||||||
|
<div class="w-100 h-100 p-5 m-auto">
|
||||||
|
```
|
||||||
|
- L'espace entre les éléments cartes est avec l'un des styles suivants :
|
||||||
|
``` html
|
||||||
|
<div class="mb-3"> <!-- margin bottom -->
|
||||||
|
<div class="mt-3"> <!-- margin top -->
|
||||||
|
<div class="me-3"> <!-- margin end/right -->
|
||||||
|
<div class="ms-3"> <!-- margin start/left -->
|
||||||
|
<div class="mx-3"> <!-- margin left and right -->
|
||||||
|
<div class="my-3"> <!-- margin top and bottom -->
|
||||||
|
<div class="m-3"> <!-- margin -->
|
||||||
|
<div class="d-flex gap-2"> <!-- gap entre les boutons -->
|
||||||
|
```
|
||||||
|
- Chaque élément est une carte afin de donner un style uniforme :
|
||||||
|
``` html
|
||||||
|
<div class="card p-3">
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
# Système de notification de l'application
|
||||||
|
## Vue d'ensemble
|
||||||
|
Le système de notification de l'application permet d'informer les utilisateurs de diverse action.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
### Composants principaux
|
||||||
|
1. **Service de Notification** : Gère la création, l'envoi et le suivi des notifications.
|
||||||
|
2. **Interface Utilisateur** : Affiche les notifications aux utilisateurs via des pops-ups, des bannières ou des emails.
|
||||||
|
3. **Template Email** : Modèles prédéfinis pour les notifications par email.
|
||||||
|
4. **Type D'action** : Enum d'action qui déclenche des notifications (ex: nouvel utilisateur, utilisateur actif, ...).
|
||||||
|
|
||||||
|
### Service de Notification
|
||||||
|
Le service de notification est responsable de la gestion des notifications. Il inclut les fonctionnalités suivantes :
|
||||||
|
- Création de notifications basées sur des événements spécifiques.
|
||||||
|
- Envoi de notifications via différents canaux (email, in-app).
|
||||||
|
- Suivi de l'état des notifications (envoyé, lu, etc.).
|
||||||
|
|
||||||
|
### Interface Utilisateur
|
||||||
|
L'interface utilisateur affiche les notifications de manière conviviale. Les notifications peuvent apparaître sous forme
|
||||||
|
de pops-ups, de bannières ou d'emails. (Possibilité d'intéragir avec les notifications)
|
||||||
|
|
||||||
|
### Template Email
|
||||||
|
Les templates email sont utilisés pour formater les notifications envoyées par email. Chaque type de notification
|
||||||
|
a son propre template pour assurer une communication claire et cohérente.
|
||||||
|
|
||||||
|
|
||||||
|
### Type d'action
|
||||||
|
```
|
||||||
|
enum ActionType: String {
|
||||||
|
case NewUser = "NEW_USER";
|
||||||
|
case ActiveUser = "ACTIVE_USER";
|
||||||
|
case PasswordReset = "PASSWORD_RESET";
|
||||||
|
case SubscriptionExpired = "SUBSCRIPTION_EXPIRED";
|
||||||
|
case OrganizationInvited = "ORGANIZATION_INVITED";
|
||||||
|
case OrganizationInactive = "ORGANIZATION_INACTIVE";
|
||||||
|
case OrganizationDeleted = "ORGANIZATION_DELETED";
|
||||||
|
case OrginizationUserInvited = "ORGANIZATION_USER_INVITED";
|
||||||
|
case UserDeleted = "USER_DELETED";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flux de travail
|
||||||
|
1. L’administrateur crée un utilisateur depuis l’interface (formulaire “Créer un utilisateur”).
|
||||||
|
2. Le contrôleur valide la requête et appelle le cas d’usage UserAdministrationService->handle(ActionType::NewUser, $admin, $payload).
|
||||||
|
3. Le service crée l’utilisateur en base avec le statut INVITED, associe l’organisation de l’admin, et génère un lien signé/jeton de setup de mot de passe (TTL).
|
||||||
|
4. Le service publie un événement de domaine UserInvitedEvent { userId, adminId, organizationId } sur Messenger (transport async).
|
||||||
|
5. Handler async A — SendUserInvitationEmailHandler:
|
||||||
|
6. Construit l’email via Symfony Mailer + Twig (emails/user_invitation.html.twig) avec le lien de définition de mot de passe.
|
||||||
|
7. Envoie le mail à l’utilisateur invité.
|
||||||
|
8. Handler async B — NotifyAdminInvitationSentHandler:
|
||||||
|
9. Crée une notification interne (Notifier, canal “in‑app”).
|
||||||
|
10. Pousse un événement temps réel via Mercure sur le topic admin/{adminId}/events avec le type INVITATION_EMAIL_SENT.
|
||||||
|
11. L’UI admin affiche un toast/bannière confirmant “Email d’invitation envoyé”.
|
||||||
|
12. L’utilisateur ouvre l’email et clique le lien de définition de mot de passe.
|
||||||
|
13. Le PasswordSetupController vérifie la signature/le jeton et la validité (TTL), affiche le formulaire, puis enregistre le nouveau mot de passe.
|
||||||
|
14. À la réussite, l’utilisateur passe au statut ACTIVE et l’action publie UserActivatedEvent { userId, adminId, organizationId } sur Messenger (async).
|
||||||
|
15. Handler async C — NotifyAdminUserActivatedHandler:
|
||||||
|
16. Crée une notification interne (Notifier, canal “in‑app”) “Compte activé”.
|
||||||
|
17. Pousse un événement Mercure sur admin/{adminId}/events avec le type USER_ACTIVATED.
|
||||||
|
18. L’UI admin met à jour la liste des membres (badge “Actif”) et affiche un toast confirmant l’activation.
|
||||||
|
19. Journalisation/Audit:
|
||||||
|
20. Chaque handler écrit une trace (succès/échec) en base ou dans un EmailLog/NotificationLog.
|
||||||
|
21. En cas d’échec d’envoi, Messenger applique la stratégie de retry puis bascule en file failed si nécessaire (tableau de bord de supervision).
|
||||||
|
22. Cas “utilisateur existant ajouté à une autre organisation”:
|
||||||
|
23. Si l’email existe déjà, on rattache l’utilisateur à la nouvelle organisation et on publie OrganizationUserInvitedEvent.
|
||||||
|
24. Handler dédié envoie un email d’information (“Vous avez été ajouté à une nouvelle organisation”) et notifie l’admin via Notifier + Mercure.
|
||||||
|
25. Cas d’actions dérivées par enum:
|
||||||
|
26. ActionType::NewUser → déclenche UserInvitedEvent (steps 3–6).
|
||||||
|
27. ActionType::ActiveUser (si activé par un flux admin) → déclenche directement UserActivatedEvent (steps 9–10).
|
||||||
|
28. ActionType::OrganizationUserInvited → flux similaire au point 12 pour la multi‑organisation.
|
||||||
|
29. Autres actions (PasswordReset, UserDeleted, etc.) suivent le même patron: contrôleur → service (match enum) → événement Messenger → handlers (Mailer/Notifier/Mercure) → UI temps réel.
|
||||||
|
|
||||||
|
## Stack technologique
|
||||||
|
- Symfony Messenger: asynchrone, retries, découplage des I/O lents.
|
||||||
|
- Symfony Mailer + Twig: emails d’invitation et d’information.
|
||||||
|
- Symfony Notifier (canal in‑app) + Mercure: notifications persistées + push temps réel vers l’UI admin.
|
||||||
|
- Enum ActionType: routage clair dans l’application, évite la logique string‑based.
|
||||||
|
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
%% Couche 1: Action initiale
|
||||||
|
A[User action event - Admin cree un utilisateur] --> B[HTTP controller API - Symfony]
|
||||||
|
B --> C[Domain service - UserAdministrationService]
|
||||||
|
C -->|Inspecte enum ActionType::NewUser| C1[Create user - status INVITED - liaison organisation - genere lien jeton mot de passe TTL]
|
||||||
|
C1 --> D[Dispatch UserInvitedEvent - userId adminId organizationId - vers Symfony Messenger bus]
|
||||||
|
|
||||||
|
%% Couche 2: Messaging / Infra
|
||||||
|
D --> E[Transport async - AMQP / Redis / Doctrine]
|
||||||
|
E --> RQ[Retry queue]
|
||||||
|
E --> FQ[Failed queue - dead letter]
|
||||||
|
E --> W[Workers Messenger]
|
||||||
|
F[Supervisor / systemd] --> W
|
||||||
|
|
||||||
|
%% Monolog transversal (logs a chaque etape)
|
||||||
|
A --> LOG_GLOBAL[Monolog - log event initial]
|
||||||
|
B --> LOG_GLOBAL
|
||||||
|
C --> LOG_GLOBAL
|
||||||
|
C1 --> LOG_GLOBAL
|
||||||
|
D --> LOG_GLOBAL
|
||||||
|
E --> LOG_GLOBAL
|
||||||
|
RQ --> LOG_GLOBAL
|
||||||
|
FQ --> LOG_GLOBAL
|
||||||
|
W --> LOG_GLOBAL
|
||||||
|
|
||||||
|
%% Handlers pour l'invitation
|
||||||
|
W --> H1[Handler A - Symfony Mailer + Twig]
|
||||||
|
H1 --> H1o[Email d'invitation avec lien setup mot de passe]
|
||||||
|
H1 --> LOG_GLOBAL
|
||||||
|
|
||||||
|
W --> H2[Handler B - Symfony Notifier in-app]
|
||||||
|
H2 --> UI1[Notification UI admin - Email d'invitation envoye]
|
||||||
|
H2 --> LOG_GLOBAL
|
||||||
|
|
||||||
|
W -. optionnel .-> WH1[Webhook HTTP sortant - invitation envoyee]
|
||||||
|
WH1 --> LOG_GLOBAL
|
||||||
|
W -. optionnel .-> SMS1[SMS gateway - SMS invitation]
|
||||||
|
SMS1 --> LOG_GLOBAL
|
||||||
|
W -. optionnel .-> PUSH1[Mobile push service - notification mobile]
|
||||||
|
PUSH1 --> LOG_GLOBAL
|
||||||
|
|
||||||
|
RQ --> METRICS[Metrics et dashboard]
|
||||||
|
FQ --> METRICS
|
||||||
|
LOG_GLOBAL --> METRICS
|
||||||
|
|
||||||
|
%% Flux activation utilisateur
|
||||||
|
subgraph Activation du compte
|
||||||
|
UA[User action event - Invite clique le lien] --> PS[HTTP controller API - PasswordSetupController]
|
||||||
|
PS -->|Verifie signature et TTL| PSOK[Set password - user status ACTIVE]
|
||||||
|
PS --> LOG_GLOBAL
|
||||||
|
PSOK --> LOG_GLOBAL
|
||||||
|
|
||||||
|
PSOK --> D2[Dispatch UserActivatedEvent - userId adminId organizationId - vers Messenger bus]
|
||||||
|
D2 --> E2[Transport async]
|
||||||
|
E2 --> RQ2[Retry queue]
|
||||||
|
E2 --> FQ2[Failed queue]
|
||||||
|
E2 --> W2[Workers Messenger]
|
||||||
|
F --> W2
|
||||||
|
|
||||||
|
D2 --> LOG_GLOBAL
|
||||||
|
E2 --> LOG_GLOBAL
|
||||||
|
RQ2 --> LOG_GLOBAL
|
||||||
|
FQ2 --> LOG_GLOBAL
|
||||||
|
W2 --> LOG_GLOBAL
|
||||||
|
|
||||||
|
W2 --> H3[Handler C - Notifier in-app]
|
||||||
|
H3 --> UI2[Notification UI admin - Compte active]
|
||||||
|
H3 --> LOG_GLOBAL
|
||||||
|
|
||||||
|
W2 -. optionnel .-> WH2[Webhook HTTP sortant - user active]
|
||||||
|
WH2 --> LOG_GLOBAL
|
||||||
|
W2 -. optionnel .-> MAIL2[Mailer ou SMS ou Push - confirmation utilisateur]
|
||||||
|
MAIL2 --> LOG_GLOBAL
|
||||||
|
|
||||||
|
RQ2 --> METRICS
|
||||||
|
FQ2 --> METRICS
|
||||||
|
end
|
||||||
|
|
||||||
|
%% Cas particulier : utilisateur existant ajoute a une nouvelle organisation
|
||||||
|
C -->|Email deja existant| SP1[Rattache a nouvelle organisation]
|
||||||
|
SP1 --> LOG_GLOBAL
|
||||||
|
SP1 --> D3[Dispatch OrganizationUserInvitedEvent]
|
||||||
|
D3 --> E3[Transport async] --> W3[Workers]
|
||||||
|
F --> W3
|
||||||
|
D3 --> LOG_GLOBAL
|
||||||
|
E3 --> LOG_GLOBAL
|
||||||
|
W3 --> LOG_GLOBAL
|
||||||
|
|
||||||
|
W3 --> M3[Mailer - ajoute a une nouvelle organisation]
|
||||||
|
M3 --> LOG_GLOBAL
|
||||||
|
W3 --> N3[Notifier in-app - toast admin Utilisateur ajoute]
|
||||||
|
N3 --> LOG_GLOBAL
|
||||||
|
W3 -. optionnel .-> WH3[Webhook ou SMS ou Mobile]
|
||||||
|
WH3 --> LOG_GLOBAL
|
||||||
|
|
||||||
|
M3 --> METRICS
|
||||||
|
N3 --> METRICS
|
||||||
|
WH3 --> METRICS
|
||||||
|
|
||||||
|
%% Styles
|
||||||
|
classDef infra fill:#e8f0fe,stroke:#5b8def,stroke-width:1px;
|
||||||
|
classDef handler fill:#dcf7e9,stroke:#2ea66a,stroke-width:1px;
|
||||||
|
classDef ui fill:#f0d9ff,stroke:#9c27b0,stroke-width:1px;
|
||||||
|
classDef audit fill:#eeeeee,stroke:#9e9e9e,stroke-width:1px;
|
||||||
|
|
||||||
|
class E,E2,E3,RQ,FQ,RQ2,FQ2,METRICS infra;
|
||||||
|
class W,W2,W3,H1,H2,H3,M3,N3 handler;
|
||||||
|
class H1o,UI1,UI2 ui;
|
||||||
|
class LOG_GLOBAL audit;
|
||||||
|
```
|
||||||
31
README.MD
|
|
@ -6,18 +6,21 @@
|
||||||
- Stimulus
|
- Stimulus
|
||||||
- Turbo
|
- Turbo
|
||||||
- Bootstrap 5.3
|
- Bootstrap 5.3
|
||||||
- Symfony UX toogle password (https://ux.symfony.com/toggle-password)
|
|
||||||
- Les icones sont gérées via symfony UX (https://ux.symfony.com/icons)
|
|
||||||
- Les icones sont prises en prioritées dans la bibliothèque bootstrap
|
|
||||||
- Les icones n'éxistants pas dans cette bibliothèques seront prises en priorité dans fontawesome regular (pour une cohérence visuelle)
|
|
||||||
- Sinon privilégier la bibliothèque ayant le visuel le plus proche
|
|
||||||
|
|
||||||
### Version 0.1 : (17/03/2025)
|
|
||||||
- Contient la logique de login mot de passe avec une entité user (email et password seuelement)
|
|
||||||
- Une base de template twig public est gérée pour les page n'ayant pas besoin de menu
|
|
||||||
- La page de login est designé
|
|
||||||
- Une base de template est gérée pour toutes les pages de l'application aya,t besoin de l'entête et du menu général
|
|
||||||
- Une ébauche de page d'accueil est en cours
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
#### Database
|
||||||
|
```bash
|
||||||
|
php bin/console doctrine:database:create
|
||||||
|
php bin/console doctrine:schema:update --force
|
||||||
|
```
|
||||||
|
#### Roles
|
||||||
|
```bash
|
||||||
|
php bin/console app:create-role USER
|
||||||
|
php bin/console app:create-role ADMIN
|
||||||
|
php bin/console app:create-role "SUPER ADMIN"
|
||||||
|
```
|
||||||
|
#### Choices.js
|
||||||
|
```bash
|
||||||
|
php bin/console importmap:require choices.js
|
||||||
|
php bin/console importmap:require choices.js/public/assets/styles/choices.min.css
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,20 @@ import './bootstrap.js';
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
import './styles/app.css';
|
import './styles/app.css';
|
||||||
import './styles/navbar.css';
|
import './styles/navbar.css';
|
||||||
|
import './styles/sidebar.css';
|
||||||
|
import './styles/choices.css'
|
||||||
|
import 'choices.js/public/assets/styles/choices.min.css';
|
||||||
|
import 'tabulator-tables/dist/css/tabulator.min.css';
|
||||||
|
import './styles/tabulator.css';
|
||||||
|
import './styles/card.css';
|
||||||
|
import './styles/notifications.css';
|
||||||
|
|
||||||
import 'bootstrap';
|
import 'bootstrap';
|
||||||
|
import './js/template.js';
|
||||||
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
|
import './js/off_canvas.js';
|
||||||
|
import './js/hoverable-collapse.js';
|
||||||
|
import './js/cookies.js';
|
||||||
|
import 'choices.js';
|
||||||
|
import 'quill'
|
||||||
|
import 'tabulator-tables'
|
||||||
|
import './js/global.js'
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
import {Controller} from '@hotwired/stimulus'
|
||||||
|
import Quill from 'quill'
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static values = {
|
||||||
|
application: String,
|
||||||
|
organization: String,
|
||||||
|
}
|
||||||
|
static targets = ['hidden', 'submitBtn']
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
// Map each editor to its toolbar and hidden field
|
||||||
|
if (document.querySelector('#editor-description')) {
|
||||||
|
this.editors = [
|
||||||
|
{
|
||||||
|
editorSelector: '#editor-description',
|
||||||
|
toolbarSelector: '#toolbar-description',
|
||||||
|
hiddenTarget: this.hiddenTargets[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
editorSelector: '#editor-descriptionSmall',
|
||||||
|
toolbarSelector: '#toolbar-descriptionSmall',
|
||||||
|
hiddenTarget: this.hiddenTargets[1],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
this.editors.forEach(({editorSelector, toolbarSelector, hiddenTarget}) => {
|
||||||
|
const quill = new Quill(editorSelector, {
|
||||||
|
modules: {
|
||||||
|
toolbar: toolbarSelector,
|
||||||
|
},
|
||||||
|
theme: 'snow',
|
||||||
|
placeholder: 'Écrivez votre texte...',
|
||||||
|
})
|
||||||
|
|
||||||
|
quill.on('text-change', () => {
|
||||||
|
hiddenTarget.value = quill.root.innerHTML
|
||||||
|
})
|
||||||
|
|
||||||
|
hiddenTarget.value = quill.root.innerHTML
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAuthorizeSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const originalText = this.submitBtnTarget.textContent;
|
||||||
|
|
||||||
|
if (!confirm(`Vous vous apprêtez à donner l'accès à ${this.organizationValue} pour ${this.applicationValue}. Êtes‑vous sûr(e) ?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitBtnTarget.textContent = 'En cours...';
|
||||||
|
this.submitBtnTarget.disabled = true;
|
||||||
|
|
||||||
|
fetch(event.target.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: new FormData(event.target)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
this.submitBtnTarget.textContent = 'Autorisé ✓';
|
||||||
|
this.submitBtnTarget.classList.replace('btn-secondary', 'btn-success');
|
||||||
|
} else {
|
||||||
|
this.submitBtnTarget.textContent = originalText;
|
||||||
|
this.submitBtnTarget.disabled = false;
|
||||||
|
alert('Erreur lors de l\'action');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.submitBtnTarget.textContent = originalText;
|
||||||
|
this.submitBtnTarget.disabled = false;
|
||||||
|
alert('Erreur lors de l\'action');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRemoveSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const originalText = this.submitBtnTarget.textContent;
|
||||||
|
|
||||||
|
if (!confirm(`Vous vous apprêtez à retirer l'accès à ${this.applicationValue} pour ${this.organizationValue}. Êtes‑vous sûr(e) ?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitBtnTarget.textContent = 'En cours...';
|
||||||
|
this.submitBtnTarget.disabled = true;
|
||||||
|
|
||||||
|
fetch(event.target.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: new FormData(event.target)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
this.submitBtnTarget.textContent = 'Retiré ✓';
|
||||||
|
this.submitBtnTarget.classList.replace('btn-secondary', 'btn-danger');
|
||||||
|
} else {
|
||||||
|
this.submitBtnTarget.textContent = originalText;
|
||||||
|
this.submitBtnTarget.disabled = false;
|
||||||
|
alert('Erreur lors de l\'action');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.submitBtnTarget.textContent = originalText;
|
||||||
|
this.submitBtnTarget.disabled = false;
|
||||||
|
alert('Erreur lors de l\'action');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,323 @@
|
||||||
|
import { Controller } from '@hotwired/stimulus';
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ['badge', 'list'];
|
||||||
|
static values = {
|
||||||
|
userId: Number,
|
||||||
|
mercureUrl: String
|
||||||
|
};
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.loadNotifications();
|
||||||
|
this.connectToMercure();
|
||||||
|
this.toastContainer = this.createToastContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this.eventSource) {
|
||||||
|
this.eventSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadNotifications() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/notifications/unread');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
this.updateBadge(data.unreadCount);
|
||||||
|
this.renderNotifications(data.notifications);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load notifications:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectToMercure() {
|
||||||
|
try {
|
||||||
|
// Fetch the JWT token and topic from the server
|
||||||
|
const response = await fetch('/mercure-token');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
|
||||||
|
// Use server-provided topic if available, otherwise fallback to default per-user topic
|
||||||
|
const topic = data.topic || `http://portail.solutions-easy.moi/notifications/user/${this.userIdValue}`;
|
||||||
|
const url = new URL(this.mercureUrlValue);
|
||||||
|
url.searchParams.append('topic', topic);
|
||||||
|
|
||||||
|
// Add authorization token as URL param if provided (Mercure can accept it this way)
|
||||||
|
if (data.token) {
|
||||||
|
url.searchParams.append('authorization', data.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.eventSource = new EventSource(url.toString());
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ Failed to create EventSource:', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventSource.onopen = () => {
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventSource.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const notification = JSON.parse(event.data);
|
||||||
|
this.handleNewNotification(notification);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('Failed to parse Mercure message data:', parseError, 'raw data:', event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventSource.onerror = (error) => {
|
||||||
|
try {
|
||||||
|
console.error('EventSource readyState:', this.eventSource.readyState);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Could not read EventSource.readyState:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventSource will automatically try to reconnect.
|
||||||
|
// If closed, log it for debugging.
|
||||||
|
try {
|
||||||
|
if (this.eventSource.readyState === EventSource.CLOSED) {
|
||||||
|
console.log();
|
||||||
|
} else if (this.eventSource.readyState === EventSource.CONNECTING) {
|
||||||
|
console.log();
|
||||||
|
} else if (this.eventSource.readyState === EventSource.OPEN) {
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error while checking EventSource state:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to Mercure:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNewNotification(notification) {
|
||||||
|
this.showToast(notification);
|
||||||
|
this.loadNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBadge(count) {
|
||||||
|
const badge = this.badgeTarget;
|
||||||
|
if (count > 0) {
|
||||||
|
badge.textContent = count > 99 ? '99+' : count;
|
||||||
|
badge.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
badge.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNotifications(notifications) {
|
||||||
|
const list = this.listTarget;
|
||||||
|
|
||||||
|
if (notifications.length === 0) {
|
||||||
|
list.innerHTML = `
|
||||||
|
<div class="text-center py-4 text-muted">
|
||||||
|
<i class="mx-0 mb-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.865 2.5a.75.75 0 0 0-1.73 0L6.5 5.5H4.75a.75.75 0 0 0 0 1.5h1.5l-.5 3H3.5a.75.75 0 0 0 0 1.5h1.5l-.635 3.135a.75.75 0 0 0 1.47.28L6.5 11.5h3l-.635 3.135a.75.75 0 0 0 1.47.28L11 11.5h1.75a.75.75 0 0 0 0-1.5h-1.5l.5-3h2.25a.75.75 0 0 0 0-1.5h-2l.635-3.135zM9.5 10l.5-3h-3l-.5 3h3z"/>
|
||||||
|
</svg>
|
||||||
|
</i>
|
||||||
|
<p class="mb-0">Aucune notification</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = notifications.map(notif => this.renderNotificationItem(notif)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNotificationItem(notification) {
|
||||||
|
const iconHtml = this.getIcon(notification.type);
|
||||||
|
const timeAgo = this.getTimeAgo(notification.createdAt);
|
||||||
|
const readClass = notification.isRead ? 'opacity-75' : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<a class="dropdown-item preview-item ${readClass}"
|
||||||
|
href="#"
|
||||||
|
data-notification-id="${notification.id}"
|
||||||
|
data-action="click->notification#markAsRead">
|
||||||
|
<div class="preview-thumbnail">
|
||||||
|
<div class="preview-icon ${this.getIconBgClass(notification.type)}">
|
||||||
|
${iconHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="preview-item-content">
|
||||||
|
<h6 class="preview-subject font-weight-normal mb-1">${this.escapeHtml(notification.title)}</h6>
|
||||||
|
<p class="font-weight-light small-text mb-0 text-muted">${this.escapeHtml(notification.message)}</p>
|
||||||
|
<p class="font-weight-light small-text mb-0 text-muted mt-1">
|
||||||
|
<small>${timeAgo}</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-link text-danger ms-2"
|
||||||
|
data-action="click->notification#deleteNotification"
|
||||||
|
data-notification-id="${notification.id}"
|
||||||
|
style="padding: 0.25rem;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||||
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIcon(type) {
|
||||||
|
const icons = {
|
||||||
|
user_joined: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/></svg>',
|
||||||
|
user_invited: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M224 0a128 128 0 1 1 0 256a128 128 0 1 1 0-256m-45.7 304h91.4c20.6 0 40.4 3.5 58.8 9.9C323 331 320 349.1 320 368c0 59.5 29.5 112.1 74.8 144H29.7C13.3 512 0 498.7 0 482.3C0 383.8 79.8 304 178.3 304M352 368a144 144 0 1 1 288 0a144 144 0 1 1-288 0m144-80c-8.8 0-16 7.2-16 16v64c0 8.8 7.2 16 16 16h48c8.8 0 16-7.2 16-16s-7.2-16-16-16h-32v-48c0-8.8-7.2-16-16-16"/></svg>',
|
||||||
|
user_accepted: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M96 128a128 128 0 1 1 256 0a128 128 0 1 1-256 0M0 482.3C0 383.8 79.8 304 178.3 304h91.4c98.5 0 178.3 79.8 178.3 178.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3M625 177L497 305c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L591 143c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>',
|
||||||
|
user_removed: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/></svg>',
|
||||||
|
user_deactivated: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/></svg>',
|
||||||
|
org_update: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/><path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319z"/></svg>',
|
||||||
|
app_access: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/></svg>',
|
||||||
|
role_changed: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/></svg>',
|
||||||
|
};
|
||||||
|
return icons[type] || icons.user_joined;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIconBgClass(type) {
|
||||||
|
const classes = {
|
||||||
|
user_joined: 'bg-primary',
|
||||||
|
user_invited: 'bg-info',
|
||||||
|
user_accepted: 'bg-primary',
|
||||||
|
user_removed: 'bg-danger',
|
||||||
|
user_deactivated: 'bg-warning',
|
||||||
|
org_update: 'bg-warning',
|
||||||
|
app_access: 'bg-primary',
|
||||||
|
role_changed: 'bg-info',
|
||||||
|
};
|
||||||
|
return classes[type] || 'bg-primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
getTimeAgo(dateString) {
|
||||||
|
if (!dateString) return '';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date)) return '';
|
||||||
|
const now = new Date();
|
||||||
|
const seconds = Math.floor((now - date) / 1000);
|
||||||
|
|
||||||
|
if (seconds < 60) return 'À l\'instant';
|
||||||
|
if (seconds < 3600) {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
return `Il y a ${mins} ${mins > 1 ? 'mins' : 'min'}`;
|
||||||
|
}
|
||||||
|
if (seconds < 86400) {
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
return `Il y a ${hours} ${hours > 1 ? 'h' : 'h'}`;
|
||||||
|
}
|
||||||
|
if (seconds < 604800) {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
return `Il y a ${days} ${days > 1 ? 'j' : 'j'}`;
|
||||||
|
}
|
||||||
|
// For older dates, show a localized date string
|
||||||
|
try {
|
||||||
|
return date.toLocaleDateString('fr-FR', {year: 'numeric', month: 'short', day: 'numeric'});
|
||||||
|
} catch (e) {
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAsRead(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const notificationId = event.currentTarget.dataset.notificationId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`/notifications/${notificationId}/read`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loadNotifications();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to mark notification as read:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAllAsRead(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch('/notifications/mark-all-read', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loadNotifications();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to mark all as read:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteNotification(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const notificationId = event.currentTarget.dataset.notificationId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`/notifications/${notificationId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loadNotifications();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete notification:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markDropdownAsRead(event) {
|
||||||
|
}
|
||||||
|
|
||||||
|
createToastContainer() {
|
||||||
|
let container = document.getElementById('notification-toast-container');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.id = 'notification-toast-container';
|
||||||
|
container.className = 'notification-toast-container';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(notification) {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'notification-toast';
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="notification-toast-icon ${this.getIconBgClass(notification.type)}">
|
||||||
|
${this.getIcon(notification.type)}
|
||||||
|
</div>
|
||||||
|
<div class="notification-toast-content">
|
||||||
|
<div class="notification-toast-title">${this.escapeHtml(notification.title)}</div>
|
||||||
|
<div class="notification-toast-message">${this.escapeHtml(notification.message)}</div>
|
||||||
|
</div>
|
||||||
|
<button class="notification-toast-close" onclick="this.parentElement.remove()">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => toast.classList.add('show'), 10);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import {Controller} from '@hotwired/stimulus'
|
||||||
|
// Important: include a build with Ajax + pagination (TabulatorFull is simplest)
|
||||||
|
import {TabulatorFull as Tabulator} from 'tabulator-tables';
|
||||||
|
import {eyeIconLink, TABULATOR_FR_LANG} from "../js/global.js";
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static values = {aws: String};
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.table();
|
||||||
|
}
|
||||||
|
|
||||||
|
table(){
|
||||||
|
const table = new Tabulator("#tabulator-org", {
|
||||||
|
// Register locales here
|
||||||
|
langs: TABULATOR_FR_LANG,
|
||||||
|
|
||||||
|
locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it)
|
||||||
|
|
||||||
|
ajaxURL: "/organization/data",
|
||||||
|
ajaxConfig: "GET",
|
||||||
|
pagination: true,
|
||||||
|
paginationMode: "remote",
|
||||||
|
paginationSize: 10,
|
||||||
|
//paginationSizeSelector: [5, 10, 20, 50], // Désactivé pour l'instant car jpp faire de jolie style
|
||||||
|
|
||||||
|
ajaxResponse: (url, params, response) => response,
|
||||||
|
paginationDataSent: { page: "page", size: "size" },
|
||||||
|
paginationDataReceived: { last_page: "last_page" },
|
||||||
|
filterMode: "remote",
|
||||||
|
|
||||||
|
ajaxURLGenerator: function(url, config, params) {
|
||||||
|
let queryParams = new URLSearchParams();
|
||||||
|
queryParams.append('page', params.page || 1);
|
||||||
|
queryParams.append('size', params.size || 10);
|
||||||
|
|
||||||
|
// Add filters
|
||||||
|
if (params.filter) {
|
||||||
|
params.filter.forEach(filter => {
|
||||||
|
queryParams.append(`filter[${filter.field}]`, filter.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${url}?${queryParams.toString()}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
ajaxSorting: true,
|
||||||
|
ajaxFiltering: true,
|
||||||
|
rowHeight: 60,
|
||||||
|
layout: "fitColumns", // activate French
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
title: "Logo",
|
||||||
|
field: "logoUrl",
|
||||||
|
formatter: "image",
|
||||||
|
formatterParams: {
|
||||||
|
height: "50px",
|
||||||
|
width: "50px",
|
||||||
|
urlPrefix: "",
|
||||||
|
urlSuffix: "",
|
||||||
|
},
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
// TODO: regarder quel style est mieux entre les "hozAlign"
|
||||||
|
// TODO: regarder quel style est mieux avec/sans headerFilter
|
||||||
|
{title: "Nom", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle", headerHozAlign: "left"},
|
||||||
|
{title: "Email", field: "email", headerFilter: "input", widthGrow: 2, vertAlign: "middle", hozAlign: "center"},
|
||||||
|
{
|
||||||
|
title: "Actions",
|
||||||
|
field: "showUrl",
|
||||||
|
hozAlign: "center",
|
||||||
|
width: 100,
|
||||||
|
vertAlign: "middle",
|
||||||
|
headerSort: false,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const url = cell.getValue();
|
||||||
|
if (url) {
|
||||||
|
return eyeIconLink(url);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,840 @@
|
||||||
|
import {Controller} from '@hotwired/stimulus';
|
||||||
|
import Choices from 'choices.js';
|
||||||
|
import {TabulatorFull as Tabulator} from 'tabulator-tables';
|
||||||
|
import {activateUserIcon, deactivateUserIcon, eyeIconLink, sendEmailIcon, TABULATOR_FR_LANG} from "../js/global.js";
|
||||||
|
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static values = {
|
||||||
|
rolesArray: Array,
|
||||||
|
selectedRoleIds: Array,
|
||||||
|
id: Number,
|
||||||
|
list: Boolean,
|
||||||
|
listOrganization: Boolean,
|
||||||
|
new: Boolean,
|
||||||
|
admin: Boolean,
|
||||||
|
listSmall: Boolean,
|
||||||
|
statut: Boolean,
|
||||||
|
orgId: Number
|
||||||
|
}
|
||||||
|
|
||||||
|
static targets = ["select"];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.roleSelect();
|
||||||
|
if (this.listValue) {
|
||||||
|
this.table();
|
||||||
|
}
|
||||||
|
if (this.newValue) {
|
||||||
|
this.tableNew();
|
||||||
|
}
|
||||||
|
if (this.adminValue) {
|
||||||
|
this.tableSmallAdmin();
|
||||||
|
}
|
||||||
|
if (this.listOrganizationValue) {
|
||||||
|
this.tableOrganization()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
roleSelect() {
|
||||||
|
if (this.hasSelectTarget) {
|
||||||
|
const choicesData = this.rolesArrayValue.map(role => ({
|
||||||
|
value: role.id,
|
||||||
|
label: role.name,
|
||||||
|
selected: this.selectedRoleIdsValue.includes(role.id)
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Choices(this.selectTarget, {
|
||||||
|
choices: choicesData,
|
||||||
|
removeItemButton: true,
|
||||||
|
placeholder: true,
|
||||||
|
placeholderValue: 'Ajouter un ou plusieurs rôles',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: vérifier le style des header filter et vertAlign/hozalign
|
||||||
|
table() {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "",
|
||||||
|
field: "isConnected",
|
||||||
|
width: 40, // small column
|
||||||
|
hozAlign: "center",
|
||||||
|
vertAlign: "middle",
|
||||||
|
headerSort: false,
|
||||||
|
tooltip: false,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const online = !!cell.getValue();
|
||||||
|
const color = online ? "#80F20E" : "#E42E31"; // green/red
|
||||||
|
return `<span class="status-dot" style="
|
||||||
|
display:inline-block;
|
||||||
|
width:10px;height:10px;
|
||||||
|
border-radius:50%;
|
||||||
|
background:${color};
|
||||||
|
"></span>`;
|
||||||
|
},
|
||||||
|
// Optional: for accessibility
|
||||||
|
formatterPrint: (cell) => (cell.getValue() ? "online" : "offline"),
|
||||||
|
formatterClipboard: (cell) => (cell.getValue() ? "online" : "offline"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Profil",
|
||||||
|
field: "pictureUrl",
|
||||||
|
width: 80,
|
||||||
|
hozAlign: "center",
|
||||||
|
headerSort: false,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const data = cell.getRow().getData();
|
||||||
|
const url = cell.getValue();
|
||||||
|
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||||
|
const initials = `${first(data.name)}${first(data.prenom)}`;
|
||||||
|
|
||||||
|
// wrapper is for centering and circle clipping
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "avatar-wrapper";
|
||||||
|
// same size for both cases
|
||||||
|
wrapper.style.width = "40px";
|
||||||
|
wrapper.style.height = "40px";
|
||||||
|
wrapper.style.display = "flex";
|
||||||
|
wrapper.style.alignItems = "center";
|
||||||
|
wrapper.style.justifyContent = "center";
|
||||||
|
wrapper.style.borderRadius = "50%";
|
||||||
|
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
wrapper.style.background = "#6c757d"; // gray background
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "avatar-initials";
|
||||||
|
span.style.color = "#fff";
|
||||||
|
span.style.fontWeight = "600";
|
||||||
|
span.style.fontSize = "14px";
|
||||||
|
span.textContent = initials || "•";
|
||||||
|
wrapper.appendChild(span);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image case: make it fill the same wrapper
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = url;
|
||||||
|
img.alt = initials || "avatar";
|
||||||
|
img.style.width = "100%";
|
||||||
|
img.style.height = "100%";
|
||||||
|
img.style.objectFit = "cover"; // keep aspect and cover circle
|
||||||
|
wrapper.appendChild(img);
|
||||||
|
|
||||||
|
// Optional: fallback if image fails
|
||||||
|
img.addEventListener("error", () => {
|
||||||
|
wrapper.innerHTML = "";
|
||||||
|
wrapper.style.background = "#6c757d";
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "avatar-initials";
|
||||||
|
span.style.color = "#fff";
|
||||||
|
span.style.fontWeight = "600";
|
||||||
|
span.style.fontSize = "12px";
|
||||||
|
span.textContent = initials || "•";
|
||||||
|
wrapper.appendChild(span);
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{title: "<b>Nom</b>", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle"},
|
||||||
|
{title: "<b>Prénom</b>", field: "prenom", headerFilter: "input", widthGrow: 2, vertAlign: "middle"},
|
||||||
|
{title: "<b>Email</b>", field: "email", headerFilter: "input", widthGrow: 3, vertAlign: "middle"},
|
||||||
|
{
|
||||||
|
title: "<b>Statut</b>", field: "statut", vertAlign: "middle",
|
||||||
|
formatter: (cell) => {
|
||||||
|
const statut = cell.getValue();
|
||||||
|
if (statut) {
|
||||||
|
return `<span class="badge bg-success">Actif</span>`
|
||||||
|
} else {
|
||||||
|
return `<span class="badge bg-secondary">Inactif</span>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "<b>Actions</b>",
|
||||||
|
field: "showUrl",
|
||||||
|
vertAlign: "middle",
|
||||||
|
headerSort: false,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const url = cell.getValue();
|
||||||
|
if (!url) return '';
|
||||||
|
|
||||||
|
const rowData = cell.getRow().getData();
|
||||||
|
const userId = rowData.id;
|
||||||
|
const statut = rowData.statut;
|
||||||
|
|
||||||
|
// Decide which action (deactivate vs activate)
|
||||||
|
const isActive = Boolean(statut);
|
||||||
|
|
||||||
|
const actionClass = isActive ? 'deactivate-user' : 'activate-user';
|
||||||
|
const actionTitle = isActive ? 'Désactiver' : 'Réactiver';
|
||||||
|
const actionColorClass = isActive ? 'color-secondary' : 'color-primary';
|
||||||
|
|
||||||
|
// SVGs
|
||||||
|
const deactivateSvg = deactivateUserIcon();
|
||||||
|
|
||||||
|
const activateSvg = activateUserIcon();
|
||||||
|
const actionSvg = isActive ? deactivateSvg : activateSvg;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="d-flex gap-2 align-content-center">
|
||||||
|
${eyeIconLink(url)}
|
||||||
|
|
||||||
|
<a href="#"
|
||||||
|
class="${actionColorClass} ${actionClass} pt-3"
|
||||||
|
data-id="${userId}"
|
||||||
|
title="${actionTitle}">
|
||||||
|
${actionSvg}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
cellClick: function (e, cell) {
|
||||||
|
const target = e.target.closest('a');
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
// Deactivate
|
||||||
|
if (target.classList.contains('deactivate-user')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const userId = target.getAttribute('data-id');
|
||||||
|
if (confirm('Voulez-vous vraiment désactiver cet utilisateur ?')) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('status', 'deactivate');
|
||||||
|
|
||||||
|
fetch(`/user/activeStatus/${userId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
const data = cell.getRow().getData();
|
||||||
|
data.statut = false;
|
||||||
|
cell.getRow().reformat();
|
||||||
|
} else {
|
||||||
|
const text = await response.text();
|
||||||
|
alert('Erreur lors de la désactivation: ' + text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => alert('Erreur lors de la désactivation'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate
|
||||||
|
if (target.classList.contains('activate-user')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const userId = target.getAttribute('data-id');
|
||||||
|
if (confirm('Voulez-vous réactiver cet utilisateur ?')) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('status','activate');
|
||||||
|
|
||||||
|
fetch(`/user/activeStatus/${userId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
// Switch status back to active and re-render row
|
||||||
|
const data = cell.getRow().getData();
|
||||||
|
data.statut = true;
|
||||||
|
cell.getRow().reformat();
|
||||||
|
} else {
|
||||||
|
const text = await response.text();
|
||||||
|
alert('Erreur lors de la réactivation: ' + text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => alert('Erreur lors de la réactivation'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
const tabulator = new Tabulator("#tabulator-userList", {
|
||||||
|
langs: TABULATOR_FR_LANG,
|
||||||
|
locale: "fr",
|
||||||
|
ajaxURL: "/user/data",
|
||||||
|
ajaxConfig: "GET",
|
||||||
|
pagination: true,
|
||||||
|
paginationMode: "remote",
|
||||||
|
paginationSize: 10,
|
||||||
|
|
||||||
|
ajaxResponse: (url, params, response) => response,
|
||||||
|
paginationDataSent: {page: "page", size: "size"},
|
||||||
|
paginationDataReceived: {last_page: "last_page"},
|
||||||
|
|
||||||
|
ajaxSorting: true,
|
||||||
|
ajaxFiltering: true,
|
||||||
|
filterMode: "remote",
|
||||||
|
|
||||||
|
// Add this to send filter data
|
||||||
|
ajaxURLGenerator: function(url, config, params) {
|
||||||
|
let queryParams = new URLSearchParams();
|
||||||
|
queryParams.append('page', params.page || 1);
|
||||||
|
queryParams.append('size', params.size || 10);
|
||||||
|
|
||||||
|
// Add filters
|
||||||
|
if (params.filter) {
|
||||||
|
params.filter.forEach(filter => {
|
||||||
|
queryParams.append(`filter[${filter.field}]`, filter.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${url}?${queryParams.toString()}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
rowHeight: 60,
|
||||||
|
layout: "fitColumns",
|
||||||
|
columns
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
tableNew() {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "Profil",
|
||||||
|
field: "pictureUrl",
|
||||||
|
width: 80,
|
||||||
|
hozAlign: "center",
|
||||||
|
headerSort: false,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const data = cell.getRow().getData();
|
||||||
|
const url = cell.getValue();
|
||||||
|
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||||
|
const initials = `${data.initials}`;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "avatar-wrapper";
|
||||||
|
// same size for both cases
|
||||||
|
wrapper.style.width = "40px";
|
||||||
|
wrapper.style.height = "40px";
|
||||||
|
wrapper.style.display = "flex";
|
||||||
|
wrapper.style.alignItems = "center";
|
||||||
|
wrapper.style.justifyContent = "center";
|
||||||
|
wrapper.style.borderRadius = "50%";
|
||||||
|
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
wrapper.style.background = "#6c757d"; // gray background
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "avatar-initials";
|
||||||
|
span.style.color = "#fff";
|
||||||
|
span.style.fontWeight = "600";
|
||||||
|
span.style.fontSize = "14px";
|
||||||
|
span.textContent = initials || "•";
|
||||||
|
wrapper.appendChild(span);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image case: make it fill the same wrapper
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = url;
|
||||||
|
img.alt = initials || "avatar";
|
||||||
|
img.style.width = "100%";
|
||||||
|
img.style.height = "100%";
|
||||||
|
img.style.objectFit = "cover"; // keep aspect and cover circle
|
||||||
|
wrapper.appendChild(img);
|
||||||
|
|
||||||
|
// Optional: fallback if image fails
|
||||||
|
img.addEventListener("error", () => {
|
||||||
|
wrapper.innerHTML = "";
|
||||||
|
wrapper.style.background = "#6c757d";
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "avatar-initials";
|
||||||
|
span.style.color = "#fff";
|
||||||
|
span.style.fontWeight = "600";
|
||||||
|
span.style.fontSize = "12px";
|
||||||
|
span.textContent = initials || "•";
|
||||||
|
wrapper.appendChild(span);
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{title: "<b>Email</b>", field: "email", widthGrow: 3, vertAlign: "middle"},
|
||||||
|
{
|
||||||
|
title: "<b>Actions</b>",
|
||||||
|
field: "showUrl",
|
||||||
|
hozAlign: "center",
|
||||||
|
width: 100,
|
||||||
|
vertAlign: "middle",
|
||||||
|
headerSort: false,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const url = cell.getValue();
|
||||||
|
if (url) {
|
||||||
|
return eyeIconLink(url);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const tabulator = new Tabulator("#tabulator-userListSmall", {
|
||||||
|
|
||||||
|
locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it)
|
||||||
|
ajaxURL: "/user/data/new",
|
||||||
|
|
||||||
|
ajaxConfig: "GET",
|
||||||
|
pagination: false,
|
||||||
|
paginationMode: "remote",
|
||||||
|
// paginationSize: 5,
|
||||||
|
ajaxParams: {orgId: this.orgIdValue},
|
||||||
|
langs: TABULATOR_FR_LANG,
|
||||||
|
ajaxResponse: (url, params, response) => response.data,
|
||||||
|
// paginationDataSent: {page: "page", size: "size"},
|
||||||
|
// paginationDataReceived: {last_page: "last_page"},
|
||||||
|
|
||||||
|
// ajaxSorting: true,
|
||||||
|
// ajaxFiltering: true,
|
||||||
|
rowHeight: 60,
|
||||||
|
layout: "fitColumns", // activate French
|
||||||
|
|
||||||
|
columns
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tableSmallAdmin() {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "Profil",
|
||||||
|
field: "pictureUrl",
|
||||||
|
width: 80,
|
||||||
|
hozAlign: "center",
|
||||||
|
headerSort: false,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const data = cell.getRow().getData();
|
||||||
|
const url = cell.getValue();
|
||||||
|
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||||
|
const initials = `${data.initials}`;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "avatar-wrapper";
|
||||||
|
// same size for both cases
|
||||||
|
wrapper.style.width = "40px";
|
||||||
|
wrapper.style.height = "40px";
|
||||||
|
wrapper.style.display = "flex";
|
||||||
|
wrapper.style.alignItems = "center";
|
||||||
|
wrapper.style.justifyContent = "center";
|
||||||
|
wrapper.style.borderRadius = "50%";
|
||||||
|
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
wrapper.style.background = "#6c757d"; // gray background
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "avatar-initials";
|
||||||
|
span.style.color = "#fff";
|
||||||
|
span.style.fontWeight = "600";
|
||||||
|
span.style.fontSize = "14px";
|
||||||
|
span.textContent = initials || "•";
|
||||||
|
wrapper.appendChild(span);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image case: make it fill the same wrapper
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = url;
|
||||||
|
img.alt = initials || "avatar";
|
||||||
|
img.style.width = "100%";
|
||||||
|
img.style.height = "100%";
|
||||||
|
img.style.objectFit = "cover"; // keep aspect and cover circle
|
||||||
|
wrapper.appendChild(img);
|
||||||
|
|
||||||
|
// Optional: fallback if image fails
|
||||||
|
img.addEventListener("error", () => {
|
||||||
|
wrapper.innerHTML = "";
|
||||||
|
wrapper.style.background = "#6c757d";
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "avatar-initials";
|
||||||
|
span.style.color = "#fff";
|
||||||
|
span.style.fontWeight = "600";
|
||||||
|
span.style.fontSize = "12px";
|
||||||
|
span.textContent = initials || "•";
|
||||||
|
wrapper.appendChild(span);
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{title: "<b>Email</b>", field: "email", widthGrow: 3, vertAlign: "middle"},
|
||||||
|
{
|
||||||
|
title: "<b>Actions</b>",
|
||||||
|
field: "showUrl",
|
||||||
|
hozAlign: "center",
|
||||||
|
width: 100,
|
||||||
|
vertAlign: "middle",
|
||||||
|
headerSort: false,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const url = cell.getValue();
|
||||||
|
if (url) {
|
||||||
|
eyeIconLink(url);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const tabulator = new Tabulator("#tabulator-userListSmallAdmin", {
|
||||||
|
|
||||||
|
locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it)
|
||||||
|
ajaxURL: "/user/data/admin",
|
||||||
|
|
||||||
|
ajaxConfig: "GET",
|
||||||
|
pagination: false,
|
||||||
|
paginationMode: "remote",
|
||||||
|
// paginationSize: 5,
|
||||||
|
ajaxParams: {orgId: this.orgIdValue},
|
||||||
|
langs: TABULATOR_FR_LANG,
|
||||||
|
ajaxResponse: (url, params, response) => response.data,
|
||||||
|
// paginationDataSent: {page: "page", size: "size"},
|
||||||
|
// paginationDataReceived: {last_page: "last_page"},
|
||||||
|
|
||||||
|
// ajaxSorting: true,
|
||||||
|
// ajaxFiltering: true,
|
||||||
|
rowHeight: 60,
|
||||||
|
layout: "fitColumns", // activate French
|
||||||
|
|
||||||
|
columns
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tableOrganization() {
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "",
|
||||||
|
field: "isConnected",
|
||||||
|
width: 40, // small column
|
||||||
|
hozAlign: "center",
|
||||||
|
vertAlign: "middle",
|
||||||
|
headerSort: false,
|
||||||
|
tooltip: false,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const online = !!cell.getValue();
|
||||||
|
const color = online ? "#80F20E" : "#E42E31"; // green/red
|
||||||
|
return `<span class="status-dot" style="
|
||||||
|
display:inline-block;
|
||||||
|
width:10px;height:10px;
|
||||||
|
border-radius:50%;
|
||||||
|
background:${color};
|
||||||
|
"></span>`;
|
||||||
|
},
|
||||||
|
// Optional: for accessibility
|
||||||
|
formatterPrint: (cell) => (cell.getValue() ? "online" : "offline"),
|
||||||
|
formatterClipboard: (cell) => (cell.getValue() ? "online" : "offline"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Profil",
|
||||||
|
field: "pictureUrl",
|
||||||
|
width: 80,
|
||||||
|
hozAlign: "center",
|
||||||
|
headerSort: false,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const data = cell.getRow().getData();
|
||||||
|
const url = cell.getValue();
|
||||||
|
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||||
|
const initials = `${first(data.name)}${first(data.prenom)}`;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "avatar-wrapper";
|
||||||
|
// same size for both cases
|
||||||
|
wrapper.style.width = "40px";
|
||||||
|
wrapper.style.height = "40px";
|
||||||
|
wrapper.style.display = "flex";
|
||||||
|
wrapper.style.alignItems = "center";
|
||||||
|
wrapper.style.justifyContent = "center";
|
||||||
|
wrapper.style.borderRadius = "50%";
|
||||||
|
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
wrapper.style.background = "#6c757d"; // gray background
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "avatar-initials";
|
||||||
|
span.style.color = "#fff";
|
||||||
|
span.style.fontWeight = "600";
|
||||||
|
span.style.fontSize = "14px";
|
||||||
|
span.textContent = initials || "•";
|
||||||
|
wrapper.appendChild(span);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image case: make it fill the same wrapper
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = url;
|
||||||
|
img.alt = initials || "avatar";
|
||||||
|
img.style.width = "100%";
|
||||||
|
img.style.height = "100%";
|
||||||
|
img.style.objectFit = "cover"; // keep aspect and cover circle
|
||||||
|
wrapper.appendChild(img);
|
||||||
|
|
||||||
|
// Optional: fallback if image fails
|
||||||
|
img.addEventListener("error", () => {
|
||||||
|
wrapper.innerHTML = "";
|
||||||
|
wrapper.style.background = "#6c757d";
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "avatar-initials";
|
||||||
|
span.style.color = "#fff";
|
||||||
|
span.style.fontWeight = "600";
|
||||||
|
span.style.fontSize = "12px";
|
||||||
|
span.textContent = initials || "•";
|
||||||
|
wrapper.appendChild(span);
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{title: "<b>Nom</b>", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle"},
|
||||||
|
{title: "<b>Prénom</b>", field: "prenom", headerFilter: "input", widthGrow: 2, vertAlign: "middle"},
|
||||||
|
{title: "<b>Email</b>", field: "email", headerFilter: "input", widthGrow: 3, vertAlign: "middle"},
|
||||||
|
{
|
||||||
|
title: "<b>Statut</b>", field: "statut", vertAlign: "middle",
|
||||||
|
formatter: (cell) => {
|
||||||
|
const statut = cell.getValue();
|
||||||
|
if (statut === "INVITED") {
|
||||||
|
return `<span class="badge bg-primary">Invité</span>`
|
||||||
|
} else if (statut === "ACTIVE") {
|
||||||
|
return `<span class="badge bg-success">Actif</span>`
|
||||||
|
}else if( statut === "EXPIRED"){
|
||||||
|
return `<span class="badge bg-warning text-dark">Expiré</span>`
|
||||||
|
} else{
|
||||||
|
return `<span class="badge bg-secondary">Inactif</span>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "<b>Actions</b>",
|
||||||
|
field: "showUrl",
|
||||||
|
vertAlign: "middle",
|
||||||
|
headerSort: false,
|
||||||
|
formatter: (cell) => {
|
||||||
|
const url = cell.getValue();
|
||||||
|
if (!url) return '';
|
||||||
|
|
||||||
|
const rowData = cell.getRow().getData();
|
||||||
|
const userId = rowData.id;
|
||||||
|
const statut = rowData.statut;
|
||||||
|
const orgId = this.orgIdValue;
|
||||||
|
|
||||||
|
// Check if user is expired
|
||||||
|
if (statut === "EXPIRED") {
|
||||||
|
return `
|
||||||
|
<div class="d-flex gap-2 align-content-center">
|
||||||
|
${eyeIconLink(url)}
|
||||||
|
|
||||||
|
${sendEmailIcon(userId, orgId)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}if (statut === "INVITED") {
|
||||||
|
return `
|
||||||
|
<div class="d-flex gap-2 align-content-center">
|
||||||
|
${eyeIconLink(url)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide which action (deactivate vs activate) for non-expired users
|
||||||
|
const isActive = (statut === "ACTIVE");
|
||||||
|
|
||||||
|
const actionClass = isActive ? 'deactivate-user' : 'activate-user';
|
||||||
|
const actionTitle = isActive ? 'Désactiver' : 'Réactiver';
|
||||||
|
const actionColorClass = isActive ? 'color-secondary' : 'color-primary';
|
||||||
|
|
||||||
|
// SVGs
|
||||||
|
const deactivateSvg = deactivateUserIcon();
|
||||||
|
|
||||||
|
const activateSvg = activateUserIcon();
|
||||||
|
|
||||||
|
const actionSvg = isActive ? deactivateSvg : activateSvg;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="d-flex gap-2 align-content-center">
|
||||||
|
${eyeIconLink(url)}
|
||||||
|
|
||||||
|
<a href="#"
|
||||||
|
class="${actionColorClass} ${actionClass} pt-3"
|
||||||
|
data-id="${userId}"
|
||||||
|
data-org-id="${orgId}"
|
||||||
|
title="${actionTitle}">
|
||||||
|
${actionSvg}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
cellClick: function (e, cell) {
|
||||||
|
const target = e.target.closest('a');
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
// Handle resend invitation for expired users
|
||||||
|
if (target.classList.contains('resend-invitation')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const userId = target.getAttribute('data-id');
|
||||||
|
if (confirm('Voulez-vous renvoyer l\'invitation à cet utilisateur ?')) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('organizationId', target.getAttribute('data-org-id'));
|
||||||
|
|
||||||
|
fetch(`/user/organization/resend-invitation/${userId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
const data = cell.getRow().getData();
|
||||||
|
data.statut = "INVITED";
|
||||||
|
cell.getRow().reformat();
|
||||||
|
alert('Invitation renvoyée avec succès');
|
||||||
|
} else {
|
||||||
|
const text = await response.text();
|
||||||
|
alert('Erreur lors de l\'envoi : ' + text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => alert('Erreur lors de l\'envoi'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate
|
||||||
|
if (target.classList.contains('deactivate-user')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const userId = target.getAttribute('data-id');
|
||||||
|
if (confirm('Voulez-vous vraiment désactiver cet utilisateur ?')) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('status', 'deactivate');
|
||||||
|
formData.append('organizationId', target.getAttribute('data-org-id'));
|
||||||
|
|
||||||
|
fetch(`/user/organization/activateStatus/${userId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
const data = cell.getRow().getData();
|
||||||
|
data.statut = "INACTIVE";
|
||||||
|
cell.getRow().reformat();
|
||||||
|
} else {
|
||||||
|
const text = await response.text();
|
||||||
|
alert('Erreur lors de la désactivation: ' + text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => alert('Erreur lors de la désactivation'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate
|
||||||
|
if (target.classList.contains('activate-user')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const userId = target.getAttribute('data-id');
|
||||||
|
if (confirm('Voulez-vous réactiver cet utilisateur ?')) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('status', 'activate');
|
||||||
|
formData.append('organizationId', target.getAttribute('data-org-id'));
|
||||||
|
|
||||||
|
fetch(`/user/organization/activateStatus/${userId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
const data = cell.getRow().getData();
|
||||||
|
data.statut = "ACTIVE";
|
||||||
|
cell.getRow().reformat();
|
||||||
|
} else {
|
||||||
|
const text = await response.text();
|
||||||
|
alert('Erreur lors de la réactivation: ' + text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => alert('Erreur lors de la réactivation'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
// if (this.statutValue) {
|
||||||
|
// columns.push(
|
||||||
|
// {
|
||||||
|
// title: "Statut", field: "role", // or any field you want
|
||||||
|
// headerSort: false,x
|
||||||
|
// hozAlign: "center",
|
||||||
|
// vertAlign: "middle",
|
||||||
|
// formatter: (cell) => {
|
||||||
|
// const row = cell.getRow();
|
||||||
|
// const current = cell.getValue() ?? "";
|
||||||
|
//
|
||||||
|
// const select = document.createElement("select");
|
||||||
|
// select.className = "table-select-action";
|
||||||
|
// // Options
|
||||||
|
// [
|
||||||
|
// {value: "", label: "Choisir..."},
|
||||||
|
// {value: "viewer", label: "Viewer"},
|
||||||
|
// {value: "editor", label: "Editor"},
|
||||||
|
// {value: "admin", label: "Admin"},
|
||||||
|
// ].forEach(opt => {
|
||||||
|
// const o = document.createElement("option");
|
||||||
|
// o.value = opt.value;
|
||||||
|
// o.textContent = opt.label;
|
||||||
|
// if (opt.value === current) o.selected = true;
|
||||||
|
// select.appendChild(o);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// // Hook change
|
||||||
|
// select.addEventListener("change", (e) => {
|
||||||
|
// this.onSelectChange(row, e.target.value);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// // Return a DOM node from a formatter → Tabulator will mount it
|
||||||
|
// return select;
|
||||||
|
// },
|
||||||
|
// // Optional: provide text for clipboard/print
|
||||||
|
// formatterClipboard: cell => cell.getValue(),
|
||||||
|
// formatterPrint: cell => cell.getValue(),
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
const tabulator = new Tabulator("#tabulator-userListOrganization", {
|
||||||
|
langs: TABULATOR_FR_LANG,
|
||||||
|
locale: "fr",
|
||||||
|
ajaxURL: "/user/data/organization",
|
||||||
|
ajaxConfig: "GET",
|
||||||
|
ajaxParams: {orgId: this.orgIdValue},
|
||||||
|
|
||||||
|
pagination: true,
|
||||||
|
paginationMode: "remote",
|
||||||
|
paginationSize: 10,
|
||||||
|
|
||||||
|
ajaxResponse: (url, params, response) => response,
|
||||||
|
paginationDataSent: {page: "page", size: "size"},
|
||||||
|
paginationDataReceived: {last_page: "last_page"},
|
||||||
|
|
||||||
|
ajaxSorting: true,
|
||||||
|
ajaxFiltering: true,
|
||||||
|
filterMode: "remote",
|
||||||
|
|
||||||
|
ajaxURLGenerator: function(url, config, params) {
|
||||||
|
let queryParams = new URLSearchParams();
|
||||||
|
// console.log("orgId:", params.orgId);
|
||||||
|
queryParams.append('orgId', params.orgId);
|
||||||
|
queryParams.append('page', params.page || 1);
|
||||||
|
queryParams.append('size', params.size || 10);
|
||||||
|
|
||||||
|
// Add filters
|
||||||
|
if (params.filter) {
|
||||||
|
params.filter.forEach(filter => {
|
||||||
|
queryParams.append(`filter[${filter.field}]`, filter.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${url}?${queryParams.toString()}`;
|
||||||
|
},
|
||||||
|
rowHeight: 60,
|
||||||
|
layout: "fitColumns", // activate French
|
||||||
|
|
||||||
|
columns
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path d="M4 2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM4 5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zM7.5 5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zM4.5 8a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5z"/><path d="M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1zm11 0H3v14h3v-2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5V15h3z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 965 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path d="M14.763.075A.5.5 0 0 1 15 .5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V14h-1v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V10a.5.5 0 0 1 .342-.474L6 7.64V4.5a.5.5 0 0 1 .276-.447l8-4a.5.5 0 0 1 .487.022M6 8.694L1 10.36V15h5zM7 15h2v-1.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5V15h2V1.309l-7 3.5z"/><path d="M2 11h1v1H2zm2 0h1v1H4zm-2 2h1v1H2zm2 0h1v1H4zm4-4h1v1H8zm2 0h1v1h-1zm-2 2h1v1H8zm2 0h1v1h-1zm2-2h1v1h-1zm0 2h1v1h-1zM8 7h1v1H8zm2 0h1v1h-1zm2 0h1v1h-1zM8 5h1v1H8zm2 0h1v1h-1zm2 0h1v1h-1zm0-2h1v1h-1z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 598 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708"/></svg>
|
||||||
|
After Width: | Height: | Size: 236 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8L4.646 2.354a.5.5 0 0 1 0-.708"/></svg>
|
||||||
|
After Width: | Height: | Size: 236 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 464a208 208 0 1 1 0-416a208 208 0 1 1 0 416m0-464a256 256 0 1 0 0 512a256 256 0 1 0 0-512m120.9 294.6c4.5-4.2 7.1-10.1 7.1-16.3c0-12.3-10-22.3-22.3-22.3H304v-96c0-17.7-14.3-32-32-32h-32c-17.7 0-32 14.3-32 32v96h-57.7c-12.3 0-22.3 10-22.3 22.3c0 6.2 2.6 12.1 7.1 16.3l107.1 99.9c3.8 3.5 8.7 5.5 13.8 5.5s10.1-2 13.8-5.5z"/></svg>
|
||||||
|
After Width: | Height: | Size: 425 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 48a208 208 0 1 1 0 416a208 208 0 1 1 0-416m0 464a256 256 0 1 0 0-512a256 256 0 1 0 0 512M151.2 217.4c-4.6 4.2-7.2 10.1-7.2 16.4c0 12.3 10 22.3 22.3 22.3H208v96c0 17.7 14.3 32 32 32h32c17.7 0 32-14.3 32-32v-96h41.7c12.3 0 22.3-10 22.3-22.3c0-6.2-2.6-12.1-7.2-16.4l-91-84c-3.8-3.5-8.7-5.4-13.9-5.4s-10.1 1.9-13.9 5.4l-91 84z"/></svg>
|
||||||
|
After Width: | Height: | Size: 428 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M406.5 399.6c-19.1-46.7-65-79.6-118.5-79.6h-64c-53.5 0-99.4 32.9-118.5 79.6C69.9 362.2 48 311.7 48 256c0-114.9 93.1-208 208-208s208 93.1 208 208c0 55.7-21.9 106.2-57.5 143.6m-40.1 32.7c-32 20.1-69.8 31.7-110.4 31.7s-78.4-11.6-110.5-31.7c7.3-36.7 39.7-64.3 78.5-64.3h64c38.8 0 71.2 27.6 78.5 64.3zM256 512a256 256 0 1 0 0-512a256 256 0 1 0 0 512m0-272a40 40 0 1 1 0-80a40 40 0 1 1 0 80m-88-40a88 88 0 1 0 176 0a88 88 0 1 0-176 0"/></svg>
|
||||||
|
After Width: | Height: | Size: 528 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256C63 286 89.6 328.5 128 364.3c41.2 38.1 94.8 67.7 160 67.7s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80M95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6M288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80h-2c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2v2c0 44.2 35.8 80 80 80m0-208a128 128 0 1 1 0 256a128 128 0 1 1 0-256"/></svg>
|
||||||
|
After Width: | Height: | Size: 787 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" fill-rule="evenodd" d="m243.07 65.728l34.263 14.684v46.42l-41.306-17.703l-107.695 61.54l116.919 66.811L256 243.623v146.285l106.667-60.952v-94.288h42.666v119.048l-10.749 6.143l-149.333 85.333l-10.584 6.048l-10.585-6.048l-149.333-85.333L64 353.716V158.289l10.749-6.142l149.333-85.333l9.224-5.271zm-29.737 324.18V268.383l-106.666-60.952v121.525zm106.666-283.24h55.163l-91.581 91.582l30.17 30.17l91.581-91.582v55.163h42.667v-128h-128z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 548 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M9 17.192V6.808L17.154 12zm1-1.842L15.289 12L10 8.65z"/></svg>
|
||||||
|
After Width: | Height: | Size: 152 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M10 16h4q.425 0 .713-.288T15 15v-2h-2v1h-2v-4h2v1h2V9q0-.425-.288-.712T14 8h-4q-.425 0-.712.288T9 9v6q0 .425.288.713T10 16m2 6q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/></svg>
|
||||||
|
After Width: | Height: | Size: 473 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M13 8V4q0-.425.288-.712T14 3h6q.425 0 .713.288T21 4v4q0 .425-.288.713T20 9h-6q-.425 0-.712-.288T13 8M3 12V4q0-.425.288-.712T4 3h6q.425 0 .713.288T11 4v8q0 .425-.288.713T10 13H4q-.425 0-.712-.288T3 12m10 8v-8q0-.425.288-.712T14 11h6q.425 0 .713.288T21 12v8q0 .425-.288.713T20 21h-6q-.425 0-.712-.288T13 20M3 20v-4q0-.425.288-.712T4 15h6q.425 0 .713.288T11 16v4q0 .425-.288.713T10 21H4q-.425 0-.712-.288T3 20m2-9h4V5H5zm10 8h4v-6h-4zm0-12h4V5h-4zM5 19h4v-2H5zm4-2"/></svg>
|
||||||
|
After Width: | Height: | Size: 560 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M11 17h2l.3-1.5q.3-.125.563-.262t.537-.338l1.45.45l1-1.7l-1.15-1q.05-.35.05-.65t-.05-.65l1.15-1l-1-1.7l-1.45.45q-.275-.2-.537-.338T13.3 8.5L13 7h-2l-.3 1.5q-.3.125-.562.263T9.6 9.1l-1.45-.45l-1 1.7l1.15 1q-.05.35-.05.65t.05.65l-1.15 1l1 1.7l1.45-.45q.275.2.538.338t.562.262zm1-3q-.825 0-1.412-.587T10 12t.588-1.412T12 10t1.413.588T14 12t-.587 1.413T12 14m-7 7q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5H5zM5 5v14z"/></svg>
|
||||||
|
After Width: | Height: | Size: 577 B |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 888 B |
|
After Width: | Height: | Size: 888 B |
|
|
@ -0,0 +1,39 @@
|
||||||
|
var application
|
||||||
|
function getApplication(){
|
||||||
|
const body = document.getElementsByTagName('body')[0];
|
||||||
|
application = body.getAttribute('data-application');
|
||||||
|
}
|
||||||
|
|
||||||
|
getApplication();
|
||||||
|
|
||||||
|
// Support pour différents systèmes de navigation
|
||||||
|
if (typeof Turbo !== 'undefined') {
|
||||||
|
document.addEventListener('turbo:load', getApplication);
|
||||||
|
document.addEventListener('turbo:render', getApplication);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support pour les applications SPA qui utilisent l'historique de navigation
|
||||||
|
window.addEventListener('popstate', getApplication);
|
||||||
|
|
||||||
|
|
||||||
|
window.setCookie = function (cname, cvalue, exdays) {
|
||||||
|
const d = new Date();
|
||||||
|
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
|
||||||
|
let expires = "expires=" + d.toUTCString();
|
||||||
|
document.cookie = application + "-" + cname + "=" + cvalue + ";" + expires + ";path=/;SameSite=Strict";
|
||||||
|
}
|
||||||
|
window.getCookie = function (cname) {
|
||||||
|
let name = application + "-" + cname + "=";
|
||||||
|
let decodedCookie = decodeURIComponent(document.cookie);
|
||||||
|
let ca = decodedCookie.split(';');
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) === ' ') {
|
||||||
|
c = c.substring(1);
|
||||||
|
}
|
||||||
|
if (c.indexOf(name) === 0) {
|
||||||
|
return c.substring(name.length, c.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
export const TABULATOR_FR_LANG = {
|
||||||
|
fr: {
|
||||||
|
ajax: {loading: "Chargement...", error: "Erreur"},
|
||||||
|
pagination: {
|
||||||
|
page_size: "Taille de page",
|
||||||
|
page_title: "Afficher la page",
|
||||||
|
first: "Premier",
|
||||||
|
first_title: "Première page",
|
||||||
|
last: "Dernier",
|
||||||
|
last_title: "Dernière page",
|
||||||
|
prev: "Précédent",
|
||||||
|
prev_title: "Page précédente",
|
||||||
|
next: "Suivant",
|
||||||
|
next_title: "Page suivante",
|
||||||
|
all: "Tout",
|
||||||
|
counter: {showing: "Affiche", of: "de", rows: "lignes", pages: "pages"},
|
||||||
|
},
|
||||||
|
headerFilters: {default: "Filtrer la colonne...", columns: {}},
|
||||||
|
data: {loading: "Chargement des données...", error: "Erreur de chargement des données"},
|
||||||
|
groups: {item: "élément", items: "éléments"},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function eyeIconLink(url) {
|
||||||
|
return `<a href="${url}" class="p-3 align-middle color-primary" title="Voir">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="35px"
|
||||||
|
height="35px"
|
||||||
|
viewBox="0 0 576 512">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256C63 286 89.6 328.5 128 364.3c41.2 38.1 94.8 67.7 160 67.7s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80M95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6M288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80h-2c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2v2c0 44.2 35.8 80 80 80m0-208a128 128 0 1 1 0 256a128 128 0 1 1 0-256"/></svg>
|
||||||
|
</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deactivateUserIcon() {
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
|
||||||
|
<path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/>
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activateUserIcon() {
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
|
||||||
|
<path fill="currentColor" d="M96 128a128 128 0 1 1 256 0a128 128 0 1 1-256 0M0 482.3C0 383.8 79.8 304 178.3 304h91.4c98.5 0 178.3 79.8 178.3 178.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3M625 177L497 305c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L591 143c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>
|
||||||
|
</svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendEmailIcon(userId, orgId) {
|
||||||
|
return `<a href="#" class="color-primary resend-invitation pt-3" data-id="${userId}" data-org-id="${orgId}" title="Renvoyer l'invitation" >
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24"><path fill="currentColor" d="M18.175 17H15q-.425 0-.712-.288T14 16t.288-.712T15 15h3.175l-.9-.9Q17 13.825 17 13.413t.3-.713q.275-.275.7-.275t.7.275l2.6 2.6q.125.125.2.312t.075.388t-.075.387t-.2.313l-2.6 2.6q-.275.275-.687.288T17.3 19.3q-.275-.275-.275-.7t.275-.7zM4 17q-.825 0-1.412-.587T2 15V5q0-.825.588-1.412T4 3h13q.825 0 1.413.588T19 5v4.075q0 .4-.3.7t-.7.3q-.425 0-.712-.288T17 9.076V6.4L10.4 11L4 6.425V15h7.075q.425 0 .713.288t.287.712t-.287.713t-.713.287zM5.45 5l4.95 3.55L15.5 5zM4 15V5z"/></svg>
|
||||||
|
</a>`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
//Open submenu on hover in compact sidebar mode and horizontal menu mode
|
||||||
|
|
||||||
|
|
||||||
|
function initSubMenu(){
|
||||||
|
var sidebar = document.querySelectorAll('.sidebar .nav-item')
|
||||||
|
sidebar.forEach(element => {
|
||||||
|
element.addEventListener('mouseenter', eventMenu)
|
||||||
|
element.addEventListener('mouseleave', eventMenu)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function eventMenu(e){
|
||||||
|
var body = document.querySelector('body');
|
||||||
|
var sidebarIconOnly = body.classList.contains("sidebar-icon-only");
|
||||||
|
var sidebarFixed = body.classList.contains("sidebar-fixed");
|
||||||
|
if (!('ontouchstart' in document.documentElement)) {
|
||||||
|
if (sidebarIconOnly) {
|
||||||
|
var menuItem = this;
|
||||||
|
if (e.type === 'mouseenter') {
|
||||||
|
menuItem.classList.add('hover-open')
|
||||||
|
} else {
|
||||||
|
menuItem.classList.remove('hover-open')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exécuter l'initialisation au chargement de la page
|
||||||
|
document.addEventListener('DOMContentLoaded', initSubMenu);
|
||||||
|
|
||||||
|
// Support pour différents systèmes de navigation
|
||||||
|
if (typeof Turbo !== 'undefined') {
|
||||||
|
document.addEventListener('turbo:load', initSubMenu);
|
||||||
|
document.addEventListener('turbo:render', initSubMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support pour les applications SPA qui utilisent l'historique de navigation
|
||||||
|
window.addEventListener('popstate', initSubMenu);
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
// Utilisation des fonctions de cookies globales définies dans app.js
|
||||||
|
|
||||||
|
// Fonction d'initialisation du template
|
||||||
|
function initTemplate() {
|
||||||
|
// Appliquer l'état du menu depuis le cookie
|
||||||
|
applyMenuState();
|
||||||
|
|
||||||
|
// Initialiser le bouton de minimisation du menu
|
||||||
|
initMinimizeButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour appliquer l'état du menu depuis le cookie
|
||||||
|
function applyMenuState() {
|
||||||
|
var body = document.querySelector('body');
|
||||||
|
var menuState = getCookie('sidebar_state');
|
||||||
|
|
||||||
|
// Si le cookie existe, appliquer l'état enregistré
|
||||||
|
if (menuState === 'collapsed') {
|
||||||
|
if (!body.classList.contains('sidebar-icon-only')) {
|
||||||
|
body.classList.add('sidebar-icon-only');
|
||||||
|
}
|
||||||
|
} else if (menuState === 'expanded') {
|
||||||
|
if (body.classList.contains('sidebar-icon-only')) {
|
||||||
|
body.classList.remove('sidebar-icon-only');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour initialiser le bouton de minimisation du menu
|
||||||
|
function initMinimizeButton() {
|
||||||
|
// Supprimer l'ancien gestionnaire d'événements s'il existe
|
||||||
|
document.querySelectorAll('[data-toggle="minimize"]').forEach(function(button) {
|
||||||
|
// Créer une copie du bouton pour supprimer tous les écouteurs d'événements
|
||||||
|
var newButton = button.cloneNode(true);
|
||||||
|
button.parentNode.replaceChild(newButton, button);
|
||||||
|
|
||||||
|
// Ajouter le nouvel écouteur d'événements
|
||||||
|
newButton.addEventListener("click", function() {
|
||||||
|
var body = document.querySelector('body');
|
||||||
|
if ((body.classList.contains('sidebar-toggle-display')) || (body.classList.contains('sidebar-absolute'))) {
|
||||||
|
body.classList.toggle('sidebar-hidden');
|
||||||
|
// Enregistrer l'état dans un cookie
|
||||||
|
var newState = body.classList.contains('sidebar-hidden') ? 'collapsed' : 'expanded';
|
||||||
|
setCookie('sidebar_state', newState, 365); // Valable 1 an
|
||||||
|
} else {
|
||||||
|
body.classList.toggle('sidebar-icon-only');
|
||||||
|
// Enregistrer l'état dans un cookie
|
||||||
|
var newState = body.classList.contains('sidebar-icon-only') ? 'collapsed' : 'expanded';
|
||||||
|
setCookie('sidebar_state', newState, 365); // Valable 1 an
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exécuter l'initialisation au chargement de la page
|
||||||
|
document.addEventListener('DOMContentLoaded', initTemplate);
|
||||||
|
|
||||||
|
// Support pour différents systèmes de navigation
|
||||||
|
if (typeof Turbo !== 'undefined') {
|
||||||
|
document.addEventListener('turbo:load', initTemplate);
|
||||||
|
document.addEventListener('turbo:render', initTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support pour les applications SPA qui utilisent l'historique de navigation
|
||||||
|
window.addEventListener('popstate', initTemplate);
|
||||||
|
|
@ -1,3 +1,15 @@
|
||||||
|
/*variable*/
|
||||||
|
:root{
|
||||||
|
--primary-blue-light : #086572;
|
||||||
|
--primary-blue-dark : #094754;
|
||||||
|
--black-font: #1D1E1C;
|
||||||
|
--delete : #E42E31;
|
||||||
|
--disable : #A3A3A3;
|
||||||
|
--check : #80F20E;
|
||||||
|
--secondary : #cc664c;
|
||||||
|
--secondary-dark : #a5543d;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -31,3 +43,105 @@ body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-body-wrapper {
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: row;
|
||||||
|
flex-direction: row;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-top: 60px;
|
||||||
|
|
||||||
|
&.full-page-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-panel {
|
||||||
|
transition: width 0.25s ease, margin 0.25s ease;
|
||||||
|
width: calc(100% - 235px);
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
background: #EEF0FD;
|
||||||
|
width: 100%;
|
||||||
|
-webkit-flex-grow: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px 2.45rem;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
-moz-transition: all 0.25s ease;
|
||||||
|
-webkit-transition: all 0.25s ease;
|
||||||
|
-ms-transition: all 0.25s ease;
|
||||||
|
font-size: calc(0.875rem - 0.05rem);
|
||||||
|
font-family: "Nunito", sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary{
|
||||||
|
background: var(--primary-blue-light);
|
||||||
|
color : #FFFFFF;
|
||||||
|
border: var(--primary-blue-dark);
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
.btn-primary:hover{
|
||||||
|
background: var(--primary-blue-dark);
|
||||||
|
color : #FFFFFF;
|
||||||
|
border: var(--primary-blue-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger{
|
||||||
|
background: var(--delete);
|
||||||
|
color : #FFFFFF;
|
||||||
|
border: var(--delete);
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-primary{
|
||||||
|
color: var(--primary-blue-light) !important;
|
||||||
|
}
|
||||||
|
.color-primary-dark{
|
||||||
|
color: var(--primary-blue-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary{
|
||||||
|
background: var(--secondary);
|
||||||
|
color : #FFFFFF;
|
||||||
|
border: var(--secondary);
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover{
|
||||||
|
background: var(--secondary-dark);
|
||||||
|
color : #FFFFFF;
|
||||||
|
border: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning{
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-secondary{
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary{
|
||||||
|
background-color: var(--primary-blue-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-warning{
|
||||||
|
background-color: var(--secondary) !important;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
.card.no-header-bg .card-header{
|
||||||
|
background-color: transparent !important;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
.choices {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input style */
|
||||||
|
.choices__inner {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--primary-blue-light);
|
||||||
|
border-radius: 0.375rem; /* same as Bootstrap `.form-control` */
|
||||||
|
padding: 0.5rem;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder */
|
||||||
|
.choices__placeholder {
|
||||||
|
color: #6c757d; /* Bootstrap muted */
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected items (tags) */
|
||||||
|
.choices__list--multiple .choices__item {
|
||||||
|
background-color: var(--primary-blue-light) !important;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
margin: 0.15rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove "x" button */
|
||||||
|
.choices__list--multiple .choices__item .choices__button {
|
||||||
|
border-left: 1px solid rgba(255,255,255,0.3);
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.choices__list--multiple .choices__item .choices__button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown list */
|
||||||
|
.choices__list--dropdown {
|
||||||
|
border: 1px solid var(--primary-blue-light);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: 0 3px 6px rgba(0,0,0,0.1);
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown options */
|
||||||
|
.choices__list--dropdown .choices__item {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover/active in dropdown */
|
||||||
|
.choices__list--dropdown .choices__item--highlighted {
|
||||||
|
background-color: var(--primary-blue-light);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
@ -69,8 +69,11 @@
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-nav-right{
|
.navbar .navbar-menu-wrapper .navbar-toggler:active,
|
||||||
flex-direction: row;
|
.navbar .navbar-menu-wrapper .navbar-toggler:focus {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar-menu-wrapper .navbar-toggler:not(.navbar-toggler-right) {
|
.navbar .navbar-menu-wrapper .navbar-toggler:not(.navbar-toggler-right) {
|
||||||
|
|
@ -82,12 +85,24 @@
|
||||||
transition: transform 0.3s linear;
|
transition: transform 0.3s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .navbar .navbar-menu-wrapper .navbar-toggler:not(.navbar-toggler-right) {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav-right{
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group{
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group .form-control{
|
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group .form-control{
|
||||||
margin-left: 0.7rem;
|
margin-left: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#navbar-search-icon > #search{
|
#navbar-search-icon > #search{
|
||||||
vertical-align: middle;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar-menu-wrapper .navbar-nav.navbar-nav-right {
|
.navbar .navbar-menu-wrapper .navbar-nav.navbar-nav-right {
|
||||||
|
|
@ -244,4 +259,16 @@
|
||||||
#logo_orga{
|
#logo_orga{
|
||||||
width:auto;
|
width:auto;
|
||||||
max-height:40px;
|
max-height:40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group .form-control{
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: #000;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#change-project{
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
.notification-toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 80px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 16px;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 400px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(400px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast.show {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast-icon svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast-message {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-toast-close:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-notif .count-notification {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: -5px;
|
||||||
|
background: var(--primary-blue-light);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
font-size: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 5px;
|
||||||
|
height: 10px;
|
||||||
|
line-height:0.5;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.preview-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.preview-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.preview-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-thumbnail {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-icon svg,
|
||||||
|
.preview-icon i {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-subject {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item-content p {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,290 @@
|
||||||
|
.sidebar {
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
|
background: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0;
|
||||||
|
width: 235px;
|
||||||
|
z-index: 11;
|
||||||
|
transition: width 0.25s ease, background 0.25s ease;
|
||||||
|
-webkit-transition: width 0.25s ease, background 0.25s ease;
|
||||||
|
-moz-transition: width 0.25s ease, background 0.25s ease;
|
||||||
|
-ms-transition: width 0.25s ease, background 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav {
|
||||||
|
overflow: hidden;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav:not(.sub-menu) {
|
||||||
|
padding-top: 1.45rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav .nav-item {
|
||||||
|
-webkit-transition-duration: 0.25s;
|
||||||
|
-moz-transition-duration: 0.25s;
|
||||||
|
-o-transition-duration: 0.25s;
|
||||||
|
transition-duration: 0.25s;
|
||||||
|
transition-property: background;
|
||||||
|
-webkit-transition-property: background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav .nav-item .collapse {
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav .nav-item.active {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0px 0px 5px 0px rgba(197, 197, 197, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav:not(.sub-menu) > .nav-item {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar > .nav:not(.sub-menu) > .nav-item:hover {
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #494949;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav .nav-item .nav-link {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0.8125rem 1.937rem 0.8125rem 1rem;
|
||||||
|
color: #848484;
|
||||||
|
border-radius: 8px;
|
||||||
|
-webkit-transition-duration: 0.45s;
|
||||||
|
-moz-transition-duration: 0.45s;
|
||||||
|
-o-transition-duration: 0.45s;
|
||||||
|
transition-duration: 0.45s;
|
||||||
|
transition-property: color;
|
||||||
|
-webkit-transition-property: color;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav .nav-item.active > .nav-link {
|
||||||
|
color:lightgrey;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav:not(.sub-menu) > .nav-item > .nav-link {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav .nav-item .nav-link i.menu-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
margin-right: 1rem;
|
||||||
|
color: #838383;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav .nav-item .nav-link i.menu-arrow {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
color: #686868;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
-webkit-transition: transform 0.3s linear;
|
||||||
|
-moz-transition: transform 0.3s linear;
|
||||||
|
-ms-transition: transform 0.3s linear;
|
||||||
|
-o-transition: transform 0.3s linear;
|
||||||
|
transition: transform 0.3s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav .nav-item .nav-link .menu-title {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav .nav-item.active > .nav-link i, .sidebar .nav .nav-item.active > .nav-link .menu-title, .sidebar .nav .nav-item.active > .nav-link .menu-arrow {
|
||||||
|
color: #494949;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav:not(.sub-menu) > .nav-item > .nav-link[aria-expanded=true] {
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
color: #494949;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav:not(.sub-menu) > .nav-item > .nav-link[aria-expanded=true] i.menu-arrow {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sidebar .nav.sub-menu {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0.25rem 1.5rem 0 2rem;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav.sub-menu .nav-item {
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #969696;
|
||||||
|
justify-content: space-between;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav.sub-menu .nav-item svg {
|
||||||
|
position: absolute;
|
||||||
|
color: #b2b2b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav.sub-menu .nav-item .nav-link {
|
||||||
|
padding: 0.7rem 1rem;
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1;
|
||||||
|
height: auto;
|
||||||
|
border-top: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav:not(.sub-menu) .nav-link:hover{
|
||||||
|
color: #494949;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.sidebar-icon-only .sidebar {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav {
|
||||||
|
overflow: visible;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .navbar .navbar-brand-wrapper {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .navbar .navbar-brand-wrapper .brand-logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .navbar .navbar-brand-wrapper .brand-logo-mini {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item .nav-link {
|
||||||
|
display: block;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item {
|
||||||
|
border-radius: 0px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item .collapse {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav:not(.sub-menu) .nav-item.active {
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: 4px 0px 7px 0px rgba(182, 185, 189, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item .nav-link i.menu-icon {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item .nav-link i.menu-arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav.sub-menu {
|
||||||
|
padding: 0 0 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav.sub-menu .nav-item .nav-link {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .main-panel {
|
||||||
|
width: calc(100% - 70px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .navbar .navbar-menu-wrapper {
|
||||||
|
width: calc(100% - 70px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-title, .sidebar-icon-only .sidebar .nav .nav-item .nav-link .badge, .sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-sub-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-title {
|
||||||
|
border-radius: 0 5px 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav:not(.sub-menu) > .nav-item:hover {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0px 0px 5px 0px rgba(197, 197, 197, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .nav:not(.sub-menu) > .nav-item:hover,
|
||||||
|
.sidebar-icon-only .nav:not(.sub-menu) > .nav-item:hover .nav-link{
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item:hover .nav-link[aria-expanded] .menu-title {
|
||||||
|
border-radius: 0 5px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .nav-link .menu-title {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 0.5rem 1.4rem;
|
||||||
|
left: 70px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: left;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 190px;
|
||||||
|
z-index: 10000;
|
||||||
|
line-height: 1.8;
|
||||||
|
-webkit-box-shadow: 4px 0px 7px 0px rgba(182, 185, 189, 0.25);
|
||||||
|
box-shadow: 4px 0px 7px 0px rgba(182, 185, 189, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .nav-link .menu-title:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .collapse,
|
||||||
|
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .collapsing {
|
||||||
|
display: block;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0 0 5px 0;
|
||||||
|
position: absolute;
|
||||||
|
left: 70px;
|
||||||
|
width: 190px;
|
||||||
|
-webkit-box-shadow: 4px 4px 7px 0px rgba(182, 185, 189, 0.25);
|
||||||
|
box-shadow: 4px 4px 7px 0px rgba(182, 185, 189, 0.25);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
|
||||||
|
/* Remove outer table border */
|
||||||
|
.tabulator {
|
||||||
|
border: none !important;
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove header and row cell borders */
|
||||||
|
.tabulator-header,
|
||||||
|
.tabulator-header .tabulator-col,
|
||||||
|
.tabulator-tableholder,
|
||||||
|
.tabulator-table,
|
||||||
|
.tabulator-row,
|
||||||
|
.tabulator-row .tabulator-cell {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove column header bottom border and row separators */
|
||||||
|
.tabulator-header {
|
||||||
|
border-bottom: none !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
/*border-top-left-radius: 25%;*/
|
||||||
|
/*border-top-right-radius: 25%;*/
|
||||||
|
}
|
||||||
|
.tabulator-row {
|
||||||
|
border-bottom: none !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove look on hover/selected without borders */
|
||||||
|
.tabulator-row:hover {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
.tabulator-row.tabulator-selected {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabulator-row.tabulator-row-odd {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounded border for images in cells */
|
||||||
|
.tabulator-cell img {
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
/* Scope to this table only */
|
||||||
|
.tabulator,
|
||||||
|
.tabulator-header,
|
||||||
|
.tabulator-header .tabulator-header-contents,
|
||||||
|
.tabulator-header .tabulator-col{
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabulator-footer {border-top: none !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabulator-footer .tabulator-page.active{
|
||||||
|
background-color: var(--primary-blue-light) !important;
|
||||||
|
border: 1px solid var(--primary-blue-light) !important;
|
||||||
|
color: #FFFFFF/* text color */ !important
|
||||||
|
}
|
||||||
|
.tabulator-footer .tabulator-page {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: 1px solid var(--primary-blue-light) !important;
|
||||||
|
color: var(--black-font)/* text color */ !important;
|
||||||
|
}
|
||||||
|
.tabulator-footer .tabulator-page:hover,
|
||||||
|
.tabulator-footer .tabulator-page.active:hover{
|
||||||
|
background-color: var(--primary-blue-dark) !important;
|
||||||
|
border: 1px solid var(--primary-blue-dark) !important;
|
||||||
|
color: #FFFFFF/* text color */ !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabulator-footer select{
|
||||||
|
border: 1px solid var(--primary-blue-light) !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: var(--black-font)/* text color */ !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabulator-header input{
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: lightgray !important;
|
||||||
|
padding-left: 15px !important;
|
||||||
|
}
|
||||||
|
.tabulator-header input::placeholder{
|
||||||
|
color: var(--black-font) !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
opacity: 1 !important; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabulator-header input:focus {
|
||||||
|
border:0;
|
||||||
|
}
|
||||||
|
.tabulator .tabulator-header .tabulator-col .tabulator-col-title {
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select hover Désactivé pour l'instant car jpp faire de jolie style */
|
||||||
|
/*#tabulator-org .tabulator-footer select:hover {*/
|
||||||
|
/* border: 1px solid var(--primary-blue-dark) !important;*/
|
||||||
|
/* background-color: var(--primary-blue-dark) !important;*/
|
||||||
|
/* color: #fff !important;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
/*.tabulator-footer select:focus {*/
|
||||||
|
/* border: 1px solid var(--primary-blue-dark) !important;*/
|
||||||
|
/* outline: none !important;*/
|
||||||
|
/* background-color: var(--primary-blue-dark) !important;*/
|
||||||
|
/* color: #fff !important;*/
|
||||||
|
/*}*/
|
||||||
|
|
@ -8,11 +8,13 @@
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
|
"aws/aws-sdk-php-symfony": "^2.8",
|
||||||
"doctrine/dbal": "^3",
|
"doctrine/dbal": "^3",
|
||||||
"doctrine/doctrine-bundle": "^2.14",
|
"doctrine/doctrine-bundle": "^2.14",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.4",
|
"doctrine/doctrine-migrations-bundle": "^3.4",
|
||||||
"doctrine/orm": "^3.3",
|
"doctrine/orm": "^3.3",
|
||||||
"firebase/php-jwt": "^6.11",
|
"firebase/php-jwt": "^6.11",
|
||||||
|
"knplabs/knp-time-bundle": "^2.4",
|
||||||
"league/oauth2-server-bundle": "^0.11.0",
|
"league/oauth2-server-bundle": "^0.11.0",
|
||||||
"nelmio/cors-bundle": "^2.5",
|
"nelmio/cors-bundle": "^2.5",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
|
|
@ -30,8 +32,9 @@
|
||||||
"symfony/intl": "7.2.*",
|
"symfony/intl": "7.2.*",
|
||||||
"symfony/mailer": "7.2.*",
|
"symfony/mailer": "7.2.*",
|
||||||
"symfony/mercure-bundle": "^0.3.9",
|
"symfony/mercure-bundle": "^0.3.9",
|
||||||
|
"symfony/messenger": "7.2.*",
|
||||||
"symfony/mime": "7.2.*",
|
"symfony/mime": "7.2.*",
|
||||||
"symfony/monolog-bundle": "^3.0",
|
"symfony/monolog-bundle": "^3.10",
|
||||||
"symfony/notifier": "7.2.*",
|
"symfony/notifier": "7.2.*",
|
||||||
"symfony/process": "7.2.*",
|
"symfony/process": "7.2.*",
|
||||||
"symfony/property-access": "7.2.*",
|
"symfony/property-access": "7.2.*",
|
||||||
|
|
@ -49,7 +52,6 @@
|
||||||
"symfony/validator": "7.2.*",
|
"symfony/validator": "7.2.*",
|
||||||
"symfony/web-link": "7.2.*",
|
"symfony/web-link": "7.2.*",
|
||||||
"symfony/yaml": "7.2.*",
|
"symfony/yaml": "7.2.*",
|
||||||
"twig/extra-bundle": "^2.12|^3.0",
|
|
||||||
"twig/twig": "^2.12|^3.0"
|
"twig/twig": "^2.12|^3.0"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ return [
|
||||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
||||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
||||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
|
||||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||||
|
|
@ -18,4 +17,6 @@ return [
|
||||||
League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true],
|
League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true],
|
||||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
||||||
|
Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true],
|
||||||
|
Aws\Symfony\AwsBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
aws:
|
||||||
|
version: latest
|
||||||
|
region: "%env(AWS_REGION)%"
|
||||||
|
credentials:
|
||||||
|
key: "%env(AWS_KEY)%"
|
||||||
|
secret: "%env(AWS_SECRET)%"
|
||||||
|
S3:
|
||||||
|
region: "%env(AWS_REGION)%"
|
||||||
|
endpoint: "%env(AWS_ENDPOINT)%"
|
||||||
|
use_path_style_endpoint: true
|
||||||
|
signature_version: 'v4'
|
||||||
|
|
@ -6,3 +6,4 @@ mercure:
|
||||||
jwt:
|
jwt:
|
||||||
secret: '%env(MERCURE_JWT_SECRET)%'
|
secret: '%env(MERCURE_JWT_SECRET)%'
|
||||||
publish: '*'
|
publish: '*'
|
||||||
|
subscribe: '*'
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@ security:
|
||||||
property: email
|
property: email
|
||||||
|
|
||||||
role_hierarchy:
|
role_hierarchy:
|
||||||
ROLE_ADMIN: ROLE_USER
|
ROLE_ADMIN: ROLE_USER
|
||||||
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
|
ROLE_SUPER_ADMIN: [ROLE_ALLOWED_TO_SWITCH, ROLE_ADMIN]
|
||||||
|
|
||||||
|
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
|
|
@ -34,7 +35,11 @@ security:
|
||||||
security: true
|
security: true
|
||||||
stateless: true
|
stateless: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
|
password_setup:
|
||||||
|
pattern: ^/password_setup
|
||||||
|
stateless: true
|
||||||
main:
|
main:
|
||||||
|
user_checker: App\Security\UserChecker
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
form_login:
|
form_login:
|
||||||
|
|
@ -57,6 +62,8 @@ security:
|
||||||
# Note: Only the *first* access control that matches will be used
|
# Note: Only the *first* access control that matches will be used
|
||||||
access_control:
|
access_control:
|
||||||
- { path: ^/login, roles: PUBLIC_ACCESS }
|
- { path: ^/login, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/password_setup, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/password_reset, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/sso_logout, roles: IS_AUTHENTICATED_FULLY }
|
- { path: ^/sso_logout, roles: IS_AUTHENTICATED_FULLY }
|
||||||
- { path: ^/token, roles: PUBLIC_ACCESS }
|
- { path: ^/token, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/oauth2/revoke_tokens, roles: PUBLIC_ACCESS }
|
- { path: ^/oauth2/revoke_tokens, roles: PUBLIC_ACCESS }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,16 @@ twig:
|
||||||
file_name_pattern: '*.twig'
|
file_name_pattern: '*.twig'
|
||||||
form_themes: ['bootstrap_5_layout.html.twig']
|
form_themes: ['bootstrap_5_layout.html.twig']
|
||||||
|
|
||||||
|
globals:
|
||||||
|
application: '%env(APPLICATION)%'
|
||||||
|
aws_url: '%env(AWS_S3_PORTAL_URL)%'
|
||||||
|
version: '0.4'
|
||||||
|
|
||||||
|
paths:
|
||||||
|
'%kernel.project_dir%/assets/img': images
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
twig:
|
twig:
|
||||||
strict_variables: true
|
strict_variables: true
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@
|
||||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
parameters:
|
parameters:
|
||||||
|
aws_url: '%env(AWS_ENDPOINT)%'
|
||||||
|
aws_public_url: '%env(AWS_ENDPOINT)%'
|
||||||
|
logos_directory: '%kernel.project_dir%/public/uploads/logos'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
|
|
@ -22,6 +25,12 @@ services:
|
||||||
App\EventSubscriber\:
|
App\EventSubscriber\:
|
||||||
resource: '../src/EventSubscriber/'
|
resource: '../src/EventSubscriber/'
|
||||||
tags: ['kernel.event_subscriber']
|
tags: ['kernel.event_subscriber']
|
||||||
|
App\Service\AwsService:
|
||||||
|
arguments:
|
||||||
|
$awsPublicUrl: '%aws_public_url%'
|
||||||
|
App\Service\OrganizationsService:
|
||||||
|
arguments:
|
||||||
|
$logoDirectory: '%logos_directory%'
|
||||||
App\EventSubscriber\ScopeResolveListener:
|
App\EventSubscriber\ScopeResolveListener:
|
||||||
tags:
|
tags:
|
||||||
- { name: kernel.event_listener, event: league.oauth2_server.event.scope_resolve, method: onScopeResolve }
|
- { name: kernel.event_listener, event: league.oauth2_server.event.scope_resolve, method: onScopeResolve }
|
||||||
|
|
|
||||||
|
|
@ -35,4 +35,42 @@ return [
|
||||||
'version' => '5.3.5',
|
'version' => '5.3.5',
|
||||||
'type' => 'css',
|
'type' => 'css',
|
||||||
],
|
],
|
||||||
|
'choices.js' => [
|
||||||
|
'version' => '11.1.0',
|
||||||
|
],
|
||||||
|
'choices.js/public/assets/styles/choices.min.css' => [
|
||||||
|
'version' => '11.1.0',
|
||||||
|
'type' => 'css',
|
||||||
|
],
|
||||||
|
'quill' => [
|
||||||
|
'version' => '2.0.3',
|
||||||
|
],
|
||||||
|
'lodash-es' => [
|
||||||
|
'version' => '4.17.21',
|
||||||
|
],
|
||||||
|
'parchment' => [
|
||||||
|
'version' => '3.0.0',
|
||||||
|
],
|
||||||
|
'quill-delta' => [
|
||||||
|
'version' => '5.1.0',
|
||||||
|
],
|
||||||
|
'eventemitter3' => [
|
||||||
|
'version' => '5.0.1',
|
||||||
|
],
|
||||||
|
'fast-diff' => [
|
||||||
|
'version' => '1.3.0',
|
||||||
|
],
|
||||||
|
'lodash.clonedeep' => [
|
||||||
|
'version' => '4.5.0',
|
||||||
|
],
|
||||||
|
'lodash.isequal' => [
|
||||||
|
'version' => '4.5.0',
|
||||||
|
],
|
||||||
|
'tabulator-tables' => [
|
||||||
|
'version' => '6.3.1',
|
||||||
|
],
|
||||||
|
'tabulator-tables/dist/css/tabulator.min.css' => [
|
||||||
|
'version' => '6.3.1',
|
||||||
|
'type' => 'css',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250709072959 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP SEQUENCE subscriptions_id_seq CASCADE');
|
||||||
|
$this->addSql('ALTER TABLE subscriptions DROP CONSTRAINT fk_4778a0167b3b43d');
|
||||||
|
$this->addSql('DROP TABLE subscriptions');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('CREATE SEQUENCE subscriptions_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
|
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX idx_4778a0167b3b43d ON subscriptions (users_id)');
|
||||||
|
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT fk_4778a0167b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250709073312 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_4778A0167B3B43D ON subscriptions (users_id)');
|
||||||
|
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT FK_4778A0167B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE subscriptions DROP CONSTRAINT FK_4778A0167B3B43D');
|
||||||
|
$this->addSql('DROP TABLE subscriptions');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250709073752 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE user_tab (id SERIAL NOT NULL, users_id INT NOT NULL, ip_address VARCHAR(255) NOT NULL, tab_id VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_98F5228767B3B43D ON user_tab (users_id)');
|
||||||
|
$this->addSql('ALTER TABLE user_tab ADD CONSTRAINT FK_98F5228767B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE user_tab DROP CONSTRAINT FK_98F5228767B3B43D');
|
||||||
|
$this->addSql('DROP TABLE user_tab');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250709115309 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP SEQUENCE subscriptions_id_seq CASCADE');
|
||||||
|
$this->addSql('ALTER TABLE subscriptions DROP CONSTRAINT fk_4778a0167b3b43d');
|
||||||
|
$this->addSql('DROP TABLE subscriptions');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('CREATE SEQUENCE subscriptions_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
|
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX idx_4778a0167b3b43d ON subscriptions (users_id)');
|
||||||
|
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT fk_4778a0167b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250709120951 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE organizations ADD name VARCHAR(255) NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE organizations DROP name');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250709121023 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE organizations ADD name VARCHAR(255) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE organizations DROP name');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250709141934 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE "user" ADD last_connection DATE DEFAULT NULL');
|
||||||
|
$this->addSql('COMMENT ON COLUMN "user".last_connection IS \'(DC2Type:date_immutable)\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE "user" DROP last_connection');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250710070735 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE "user" ALTER last_connection TYPE DATE');
|
||||||
|
$this->addSql('COMMENT ON COLUMN "user".last_connection IS NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE "user" ALTER last_connection TYPE DATE');
|
||||||
|
$this->addSql('COMMENT ON COLUMN "user".last_connection IS \'(DC2Type:date_immutable)\'');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250710071344 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE "user" ALTER last_connection TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE "user" ALTER last_connection TYPE DATE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250710090534 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE "user" ADD phone_number VARCHAR(20) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE "user" DROP phone_number');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250716083017 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE users_organizations ADD created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP');
|
||||||
|
$this->addSql('COMMENT ON COLUMN users_organizations.created_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE users_organizations DROP created_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250716130850 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250724133531 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE user_organization_app (id SERIAL NOT NULL, applications_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, users_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_BEF66DF129A0022 ON user_organization_app (applications_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_BEF66DF132C8A3DE ON user_organization_app (organization_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_BEF66DF167B3B43D ON user_organization_app (users_id)');
|
||||||
|
$this->addSql('CREATE TABLE user_organization_roles (id SERIAL NOT NULL, role_id INT DEFAULT NULL, users_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_94FD2EFBD60322AC ON user_organization_roles (role_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_94FD2EFB67B3B43D ON user_organization_roles (users_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_94FD2EFB32C8A3DE ON user_organization_roles (organization_id)');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF129A0022 FOREIGN KEY (applications_id) REFERENCES apps (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF132C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF167B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT FK_94FD2EFBD60322AC FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT FK_94FD2EFB67B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT FK_94FD2EFB32C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF129A0022');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF132C8A3DE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF167B3B43D');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT FK_94FD2EFBD60322AC');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT FK_94FD2EFB67B3B43D');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT FK_94FD2EFB32C8A3DE');
|
||||||
|
$this->addSql('DROP TABLE user_organization_app');
|
||||||
|
$this->addSql('DROP TABLE user_organization_roles');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250725065027 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP SEQUENCE user_organization_roles_id_seq CASCADE');
|
||||||
|
$this->addSql('DROP SEQUENCE user_organization_app_id_seq CASCADE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT fk_bef66df129a0022');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT fk_bef66df132c8a3de');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT fk_bef66df167b3b43d');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT fk_94fd2efb32c8a3de');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT fk_94fd2efb67b3b43d');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT fk_94fd2efbd60322ac');
|
||||||
|
$this->addSql('DROP TABLE user_organization_app');
|
||||||
|
$this->addSql('DROP TABLE user_organization_roles');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('CREATE SEQUENCE user_organization_roles_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
|
$this->addSql('CREATE SEQUENCE user_organization_app_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
|
$this->addSql('CREATE TABLE user_organization_app (id SERIAL NOT NULL, applications_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, users_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX idx_bef66df129a0022 ON user_organization_app (applications_id)');
|
||||||
|
$this->addSql('CREATE INDEX idx_bef66df132c8a3de ON user_organization_app (organization_id)');
|
||||||
|
$this->addSql('CREATE INDEX idx_bef66df167b3b43d ON user_organization_app (users_id)');
|
||||||
|
$this->addSql('CREATE TABLE user_organization_roles (id SERIAL NOT NULL, role_id INT DEFAULT NULL, users_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX idx_94fd2efb32c8a3de ON user_organization_roles (organization_id)');
|
||||||
|
$this->addSql('CREATE INDEX idx_94fd2efb67b3b43d ON user_organization_roles (users_id)');
|
||||||
|
$this->addSql('CREATE INDEX idx_94fd2efbd60322ac ON user_organization_roles (role_id)');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT fk_bef66df129a0022 FOREIGN KEY (applications_id) REFERENCES apps (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT fk_bef66df132c8a3de FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT fk_bef66df167b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT fk_94fd2efb32c8a3de FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT fk_94fd2efb67b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT fk_94fd2efbd60322ac FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250804084150 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE actions ADD organization_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE actions ADD CONSTRAINT FK_548F1EF32C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_548F1EF32C8A3DE ON actions (organization_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE actions DROP CONSTRAINT FK_548F1EF32C8A3DE');
|
||||||
|
$this->addSql('DROP INDEX IDX_548F1EF32C8A3DE');
|
||||||
|
$this->addSql('ALTER TABLE actions DROP organization_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250804085615 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP INDEX uniq_548f1ef67b3b43d');
|
||||||
|
$this->addSql('CREATE INDEX IDX_548F1EF67B3B43D ON actions (users_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('DROP INDEX IDX_548F1EF67B3B43D');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_548f1ef67b3b43d ON actions (users_id)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250804101742 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE actions ADD description VARCHAR(255) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE actions DROP description');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250804121445 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE apps ADD description_small VARCHAR(255) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE apps DROP description_small');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250808085504 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE user_organizaton_app (id SERIAL NOT NULL, users_id INT NOT NULL, organization_id INT DEFAULT NULL, role_id INT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, is_active BOOLEAN NOT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2C952FC767B3B43D ON user_organizaton_app (users_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2C952FC732C8A3DE ON user_organizaton_app (organization_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2C952FC7D60322AC ON user_organizaton_app (role_id)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN user_organizaton_app.created_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT FK_2C952FC767B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT FK_2C952FC732C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT FK_2C952FC7D60322AC FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE users_organizations_apps DROP CONSTRAINT fk_f01f6897964985f0');
|
||||||
|
$this->addSql('ALTER TABLE users_organizations_apps DROP CONSTRAINT fk_f01f6897a2d76671');
|
||||||
|
$this->addSql('ALTER TABLE apps_organizations DROP CONSTRAINT fk_ffe659d586288a55');
|
||||||
|
$this->addSql('ALTER TABLE apps_organizations DROP CONSTRAINT fk_ffe659d5a2d76671');
|
||||||
|
$this->addSql('DROP TABLE users_organizations_apps');
|
||||||
|
$this->addSql('DROP TABLE apps_organizations');
|
||||||
|
$this->addSql('ALTER TABLE users_organizations DROP CONSTRAINT fk_4b991472d60322ac');
|
||||||
|
$this->addSql('DROP INDEX idx_4b991472d60322ac');
|
||||||
|
$this->addSql('ALTER TABLE users_organizations DROP role_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('CREATE TABLE users_organizations_apps (users_organizations_id INT NOT NULL, apps_id INT NOT NULL, PRIMARY KEY(users_organizations_id, apps_id))');
|
||||||
|
$this->addSql('CREATE INDEX idx_f01f6897964985f0 ON users_organizations_apps (users_organizations_id)');
|
||||||
|
$this->addSql('CREATE INDEX idx_f01f6897a2d76671 ON users_organizations_apps (apps_id)');
|
||||||
|
$this->addSql('CREATE TABLE apps_organizations (apps_id INT NOT NULL, organizations_id INT NOT NULL, PRIMARY KEY(apps_id, organizations_id))');
|
||||||
|
$this->addSql('CREATE INDEX idx_ffe659d586288a55 ON apps_organizations (organizations_id)');
|
||||||
|
$this->addSql('CREATE INDEX idx_ffe659d5a2d76671 ON apps_organizations (apps_id)');
|
||||||
|
$this->addSql('ALTER TABLE users_organizations_apps ADD CONSTRAINT fk_f01f6897964985f0 FOREIGN KEY (users_organizations_id) REFERENCES users_organizations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE users_organizations_apps ADD CONSTRAINT fk_f01f6897a2d76671 FOREIGN KEY (apps_id) REFERENCES apps (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE apps_organizations ADD CONSTRAINT fk_ffe659d586288a55 FOREIGN KEY (organizations_id) REFERENCES organizations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE apps_organizations ADD CONSTRAINT fk_ffe659d5a2d76671 FOREIGN KEY (apps_id) REFERENCES apps (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT FK_2C952FC767B3B43D');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT FK_2C952FC732C8A3DE');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT FK_2C952FC7D60322AC');
|
||||||
|
$this->addSql('DROP TABLE user_organizaton_app');
|
||||||
|
$this->addSql('ALTER TABLE users_organizations ADD role_id INT NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE users_organizations ADD CONSTRAINT fk_4b991472d60322ac FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX idx_4b991472d60322ac ON users_organizations (role_id)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250808091021 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT fk_2c952fc732c8a3de');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT fk_2c952fc767b3b43d');
|
||||||
|
$this->addSql('DROP INDEX idx_2c952fc732c8a3de');
|
||||||
|
$this->addSql('DROP INDEX idx_2c952fc767b3b43d');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app DROP organization_id');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app RENAME COLUMN users_id TO user_organization_id');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT FK_2C952FC72014CF51 FOREIGN KEY (user_organization_id) REFERENCES users_organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2C952FC72014CF51 ON user_organizaton_app (user_organization_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT FK_2C952FC72014CF51');
|
||||||
|
$this->addSql('DROP INDEX IDX_2C952FC72014CF51');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app ADD organization_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app RENAME COLUMN user_organization_id TO users_id');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT fk_2c952fc732c8a3de FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT fk_2c952fc767b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX idx_2c952fc732c8a3de ON user_organizaton_app (organization_id)');
|
||||||
|
$this->addSql('CREATE INDEX idx_2c952fc767b3b43d ON user_organizaton_app (users_id)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250811121053 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app ADD application_id INT NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app ADD CONSTRAINT FK_2C952FC73E030ACD FOREIGN KEY (application_id) REFERENCES apps (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2C952FC73E030ACD ON user_organizaton_app (application_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app DROP CONSTRAINT FK_2C952FC73E030ACD');
|
||||||
|
$this->addSql('DROP INDEX IDX_2C952FC73E030ACD');
|
||||||
|
$this->addSql('ALTER TABLE user_organizaton_app DROP application_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250812113220 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE apps_organizations (apps_id INT NOT NULL, organizations_id INT NOT NULL, PRIMARY KEY(apps_id, organizations_id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_FFE659D5A2D76671 ON apps_organizations (apps_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_FFE659D586288A55 ON apps_organizations (organizations_id)');
|
||||||
|
$this->addSql('ALTER TABLE apps_organizations ADD CONSTRAINT FK_FFE659D5A2D76671 FOREIGN KEY (apps_id) REFERENCES apps (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE apps_organizations ADD CONSTRAINT FK_FFE659D586288A55 FOREIGN KEY (organizations_id) REFERENCES organizations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE apps_organizations DROP CONSTRAINT FK_FFE659D5A2D76671');
|
||||||
|
$this->addSql('ALTER TABLE apps_organizations DROP CONSTRAINT FK_FFE659D586288A55');
|
||||||
|
$this->addSql('DROP TABLE apps_organizations');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20251008081943 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE apps ALTER description TYPE TEXT');
|
||||||
|
$this->addSql('ALTER TABLE apps ALTER description DROP NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE apps ALTER description_small TYPE TEXT');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE apps ALTER description TYPE VARCHAR(255)');
|
||||||
|
$this->addSql('ALTER TABLE apps ALTER description SET NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE apps ALTER description_small TYPE VARCHAR(255)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20251013133256 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE "user" ALTER picture_url DROP NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE "user" ALTER picture_url SET NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20251028154635 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE users_organizations ADD statut VARCHAR(255) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE users_organizations DROP statut');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20251029104354 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE "user" ADD password_token VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE "user" ADD token_expiry TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('COMMENT ON COLUMN "user".token_expiry IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE "user" DROP password_token');
|
||||||
|
$this->addSql('ALTER TABLE "user" DROP token_expiry');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20251029104801 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE "user" ALTER password DROP NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE "user" ALTER password SET NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20251104081124 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE apps ADD logo_mini_url VARCHAR(255) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE apps DROP logo_mini_url');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20251105083809 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE users_organizations ADD modified_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('COMMENT ON COLUMN users_organizations.modified_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE users_organizations DROP modified_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20251117104819 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE notifications (id SERIAL NOT NULL, user_id INT NOT NULL, organization_id INT DEFAULT NULL, type VARCHAR(50) NOT NULL, title VARCHAR(255) NOT NULL, message TEXT NOT NULL, data JSON DEFAULT NULL, is_read BOOLEAN DEFAULT false NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, read_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_6000B0D3A76ED395 ON notifications (user_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_6000B0D332C8A3DE ON notifications (organization_id)');
|
||||||
|
$this->addSql('CREATE INDEX idx_user_read_created ON notifications (user_id, is_read, created_at)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN notifications.created_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN notifications.read_at IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE notifications ADD CONSTRAINT FK_6000B0D3A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE notifications ADD CONSTRAINT FK_6000B0D332C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE notifications DROP CONSTRAINT FK_6000B0D3A76ED395');
|
||||||
|
$this->addSql('ALTER TABLE notifications DROP CONSTRAINT FK_6000B0D332C8A3DE');
|
||||||
|
$this->addSql('DROP TABLE notifications');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20251117125146 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\Roles; // ⚡ your Roles entity
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:create-role',
|
||||||
|
description: 'Creates a new role in the database'
|
||||||
|
)]
|
||||||
|
class CreateRoleCommand extends Command
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addArgument('name', InputArgument::REQUIRED, 'The name of the role'); // role name required
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$roleName = trim($input->getArgument('name'));
|
||||||
|
$roleName = strtoupper($roleName); // Normalize to uppercase
|
||||||
|
|
||||||
|
// Ensure not empty
|
||||||
|
if ($roleName === '') {
|
||||||
|
$output->writeln('<error>The role name cannot be empty</error>');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if role already exists
|
||||||
|
$existing = $this->entityManager->getRepository(Roles::class)
|
||||||
|
->findOneBy(['name' => $roleName]);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$output->writeln("<comment>Role '{$roleName}' already exists.</comment>");
|
||||||
|
return Command::SUCCESS; // not failure, just redundant
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and persist new role
|
||||||
|
$role = new Roles();
|
||||||
|
$role->setName($roleName);
|
||||||
|
|
||||||
|
$this->entityManager->persist($role);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$output->writeln("<info>Role '{$roleName}' created successfully!</info>");
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\Roles;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:delete-role',
|
||||||
|
description: 'Deletes a role from the database'
|
||||||
|
)]
|
||||||
|
class DeleteRoleCommand extends Command
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addArgument('name', InputArgument::REQUIRED, 'The name of the role to delete');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$roleName = trim($input->getArgument('name'));
|
||||||
|
$roleName = strtoupper($roleName); // Normalize to uppercase
|
||||||
|
|
||||||
|
if ($roleName === '') {
|
||||||
|
$output->writeln('<error>The role name cannot be empty</error>');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the role
|
||||||
|
$role = $this->entityManager->getRepository(Roles::class)
|
||||||
|
->findOneBy(['name' => $roleName]);
|
||||||
|
|
||||||
|
if (!$role) {
|
||||||
|
$output->writeln("<error>Role '{$roleName}' not found.</error>");
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if role is being used (optional safety check)
|
||||||
|
$usageCount = $this->entityManager->getRepository(\App\Entity\UserOrganizatonApp::class)
|
||||||
|
->count(['role' => $role]);
|
||||||
|
|
||||||
|
if ($usageCount > 0) {
|
||||||
|
$output->writeln("<error>Cannot delete role '{$roleName}' - it is assigned to {$usageCount} user(s).</error>");
|
||||||
|
$output->writeln('<comment>Remove all assignments first, then try again.</comment>');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmation prompt
|
||||||
|
$helper = $this->getHelper('question');
|
||||||
|
$question = new ConfirmationQuestion(
|
||||||
|
"Are you sure you want to delete role '{$roleName}'? [y/N] ",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$helper->ask($input, $output, $question)) {
|
||||||
|
$output->writeln('<comment>Operation cancelled.</comment>');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the role
|
||||||
|
$this->entityManager->remove($role);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$output->writeln("<info>Role '{$roleName}' deleted successfully!</info>");
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Apps;
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use App\Service\ActionService;
|
||||||
|
use App\Service\UserService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route(path: '/application', name: 'application_')]
|
||||||
|
|
||||||
|
class ApplicationController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly UserService $userService, private readonly ActionService $actionService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||||
|
$applications = $this->entityManager->getRepository(Apps::class)->findAll();
|
||||||
|
|
||||||
|
return $this->render('application/index.html.twig', [
|
||||||
|
'applications' => $applications,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
|
||||||
|
public function edit(int $id, Request $request): Response{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||||
|
if (!$application) {
|
||||||
|
$this->addFlash('error', "L'application n'existe pas ou n'est pas reconnu.");
|
||||||
|
return $this->redirectToRoute('application_index');
|
||||||
|
}
|
||||||
|
$applicationData = [
|
||||||
|
'id' => $application->getId(),
|
||||||
|
'name' => $application->getName(),
|
||||||
|
'description' => $application->getDescription(),
|
||||||
|
'descriptionSmall' => $application->getDescriptionSmall(),
|
||||||
|
'isActive' => $application->isActive(),
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$data = $request->request->all();
|
||||||
|
$application->setName($data['name']);
|
||||||
|
$application->setDescription($data['description']);
|
||||||
|
$application->setDescriptionSmall($data['descriptionSmall']);
|
||||||
|
$this->entityManager->persist($application);
|
||||||
|
$this->actionService->createAction("Modification de l'application ", $actingUser, null, $application->getId());
|
||||||
|
|
||||||
|
return $this->redirectToRoute('application_index');
|
||||||
|
}
|
||||||
|
return $this->render('application/edit.html.twig', [
|
||||||
|
'apps' => $applicationData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/authorize/{id}', name: 'authorize', methods: ['POST'])]
|
||||||
|
public function authorize(int $id, Request $request)
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||||
|
if (!$application) {
|
||||||
|
throw $this->createNotFoundException("L'application n'existe pas.");
|
||||||
|
}
|
||||||
|
$orgId = $request->get('organizationId');
|
||||||
|
|
||||||
|
$organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
|
||||||
|
$application->addOrganization($organization);
|
||||||
|
|
||||||
|
$this->actionService->createAction("Authorization d'accès", $actingUser, $organization, $application->getName());
|
||||||
|
return new Response('', Response::HTTP_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/remove/{id}', name: 'remove', methods: ['POST'])]
|
||||||
|
public function remove(int $id, Request $request)
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||||
|
if (!$application) {
|
||||||
|
throw $this->createNotFoundException("L'application n'existe pas.");
|
||||||
|
}
|
||||||
|
$orgId = $request->get('organizationId');
|
||||||
|
$organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
|
||||||
|
$application->removeOrganization($organization);
|
||||||
|
|
||||||
|
$this->actionService->createAction("Authorization retirer", $actingUser, $organization, $application->getName());
|
||||||
|
|
||||||
|
return new Response('', Response::HTTP_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,9 +12,15 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||||
final class IndexController extends AbstractController
|
final class IndexController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/', name: 'app_index')]
|
#[Route('/', name: 'app_index')]
|
||||||
public function index(Request $request, LoggerInterface $logger): Response
|
public function index(): Response
|
||||||
{
|
{
|
||||||
$logger->info("SESSION ID: " . $request->getSession()->getId());
|
if ($this->isGranted('ROLE_ADMIN')) {
|
||||||
|
return $this->redirectToRoute('organization_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->isGranted('ROLE_USER')) {
|
||||||
|
return $this->redirectToRoute('application_index');
|
||||||
|
}
|
||||||
return $this->render('index/index.html.twig', [
|
return $this->render('index/index.html.twig', [
|
||||||
'controller_name' => 'IndexController',
|
'controller_name' => 'IndexController',
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Service\UserService;
|
||||||
|
use Lcobucci\JWT\Configuration;
|
||||||
|
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||||
|
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
class MercureController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(private readonly UserService $userService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
#[Route(path: '/mercure-token', name: 'mercure_token', methods: ['GET'])]
|
||||||
|
public function getMercureToken(): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||||
|
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
$topic = sprintf('http://portail.solutions-easy.moi/notifications/user/%d', $user->getId());
|
||||||
|
|
||||||
|
// Generate JWT token for Mercure subscription
|
||||||
|
$secret = $_ENV['MERCURE_JWT_SECRET'];
|
||||||
|
|
||||||
|
$config = Configuration::forSymmetricSigner(
|
||||||
|
new Sha256(),
|
||||||
|
InMemory::plainText($secret)
|
||||||
|
);
|
||||||
|
|
||||||
|
$token = $config->builder()
|
||||||
|
->withClaim('mercure', [
|
||||||
|
'subscribe' => [$topic]
|
||||||
|
])
|
||||||
|
->getToken($config->signer(), $config->signingKey());
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'token' => $token->toString(),
|
||||||
|
'topic' => $topic,
|
||||||
|
'userId' => $user->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Notification;
|
||||||
|
use App\Repository\NotificationRepository;
|
||||||
|
use App\Service\UserService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Lcobucci\JWT\Configuration;
|
||||||
|
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||||
|
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route(path: '/notifications', name: 'notification_')]
|
||||||
|
class NotificationController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly NotificationRepository $notificationRepository,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly UserService $userService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||||
|
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
$notifications = $this->notificationRepository->findRecentByUser($user, 50);
|
||||||
|
$unreadCount = $this->notificationRepository->countUnreadByUser($user);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'notifications' => array_map(fn($n) => $n->toArray(), $notifications),
|
||||||
|
'unreadCount' => $unreadCount,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/unread', name: 'unread', methods: ['GET'])]
|
||||||
|
public function unread(): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||||
|
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
$notifications = $this->notificationRepository->findUnreadByUser($user);
|
||||||
|
$unreadCount = count($notifications);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'notifications' => array_map(fn($n) => $n->toArray(), $notifications),
|
||||||
|
'unreadCount' => $unreadCount,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/count', name: 'count', methods: ['GET'])]
|
||||||
|
public function count(): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||||
|
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
$unreadCount = $this->notificationRepository->countUnreadByUser($user);
|
||||||
|
|
||||||
|
return new JsonResponse(['count' => $unreadCount]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/{id}/read', name: 'mark_read', methods: ['POST'])]
|
||||||
|
public function markAsRead(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||||
|
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
$notification = $this->notificationRepository->find($id);
|
||||||
|
|
||||||
|
if (!$notification || $notification->getUser()->getId() !== $user->getId()) {
|
||||||
|
return new JsonResponse(['error' => 'Notification not found'], Response::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$notification->setIsRead(true);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new JsonResponse(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/mark-all-read', name: 'mark_all_read', methods: ['POST'])]
|
||||||
|
public function markAllAsRead(): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||||
|
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
$count = $this->notificationRepository->markAllAsReadForUser($user);
|
||||||
|
|
||||||
|
return new JsonResponse(['success' => true, 'count' => $count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/{id}', name: 'delete', methods: ['DELETE'])]
|
||||||
|
public function delete(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||||
|
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
$notification = $this->notificationRepository->find($id);
|
||||||
|
|
||||||
|
if (!$notification || $notification->getUser()->getId() !== $user->getId()) {
|
||||||
|
return new JsonResponse(['error' => 'Notification not found'], Response::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->remove($notification);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new JsonResponse(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Actions;
|
||||||
|
use App\Entity\Apps;
|
||||||
|
use App\Entity\Roles;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Entity\UserOrganizatonApp;
|
||||||
|
use App\Entity\UsersOrganizations;
|
||||||
|
use App\Form\OrganizationForm;
|
||||||
|
use App\Repository\OrganizationsRepository;
|
||||||
|
use App\Service\ActionService;
|
||||||
|
use App\Service\AwsService;
|
||||||
|
use App\Service\OrganizationsService;
|
||||||
|
use App\Service\UserOrganizationService;
|
||||||
|
use App\Service\UserService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Exception;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use App\Entity\Organizations;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
#[Route(path: '/organization', name: 'organization_')]
|
||||||
|
class OrganizationController extends AbstractController
|
||||||
|
{
|
||||||
|
private const NOT_FOUND = 'Entity not found';
|
||||||
|
private const ACCESS_DENIED = 'Access denied';
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly UserService $userService,
|
||||||
|
private readonly OrganizationsService $organizationsService,
|
||||||
|
private readonly ActionService $actionService,
|
||||||
|
private readonly UserOrganizationService $userOrganizationService,
|
||||||
|
private readonly OrganizationsRepository $organizationsRepository,
|
||||||
|
private readonly AwsService $awsService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
if ($this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||||
|
$organizations = $this->organizationsRepository->findBy(['isDeleted' => false]);
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//get all the UO of the user
|
||||||
|
$uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
|
||||||
|
$organizations = [];
|
||||||
|
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||||
|
foreach ($uos as $uo) {
|
||||||
|
$uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin]);
|
||||||
|
if ($uoaAdmin) {
|
||||||
|
$organizations[] = $uo->getOrganization();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count($organizations) === 1 && $organizations[0]->isActive() === true) {
|
||||||
|
return $this->redirectToRoute('organization_show', ['id' => $organizations[0]->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// Map the entities for tabulator
|
||||||
|
$organizationsData = array_map(function ($org) {
|
||||||
|
return [
|
||||||
|
'id' => $org->getId(),
|
||||||
|
'name' => $org->getName(),
|
||||||
|
'email' => $org->getEmail(),
|
||||||
|
'logoUrl' => $org->getLogoUrl() ? $org->getLogoUrl() : null,
|
||||||
|
'active' => $org->isActive(),
|
||||||
|
'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]),
|
||||||
|
];
|
||||||
|
}, $organizations);
|
||||||
|
return $this->render('organization/index.html.twig', [
|
||||||
|
'organizationsData' => $organizationsData,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/new', name: 'new', methods: ['GET', 'POST'])]
|
||||||
|
public function new(Request $request): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$organization = new Organizations();
|
||||||
|
$form = $this->createForm(OrganizationForm::class, $organization);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$logoFile = $form->get('logoUrl')->getData();
|
||||||
|
if ($logoFile) {
|
||||||
|
$this->organizationsService->handleLogo($organization, $logoFile);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$this->entityManager->persist($organization);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->actionService->createAction("Create Organization", $actingUser, $organization, $organization->getName());
|
||||||
|
return $this->redirectToRoute('organization_index');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->addFlash('error', 'Error creating organization: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this->render('organization/new.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(OrganizationForm::class);
|
||||||
|
return $this->render('organization/new.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
|
||||||
|
public function edit(Request $request, $id): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
$organization = $this->organizationsRepository->find($id);
|
||||||
|
if (!$organization) {
|
||||||
|
$this->addFlash('error', self::NOT_FOUND);
|
||||||
|
return $this->redirectToRoute('organization_index');
|
||||||
|
}
|
||||||
|
if (!$this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||||
|
//check if the user is admin of the organization
|
||||||
|
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $user, 'organization' => $organization]);
|
||||||
|
if (!$uo) {
|
||||||
|
$this->addFlash('error', self::ACCESS_DENIED);
|
||||||
|
return $this->redirectToRoute('organization_index');
|
||||||
|
}
|
||||||
|
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||||
|
$uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin]);
|
||||||
|
if (!$uoaAdmin) {
|
||||||
|
$this->addFlash('error', self::ACCESS_DENIED);
|
||||||
|
return $this->redirectToRoute('organization_index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$form = $this->createForm(OrganizationForm::class, $organization);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$logoFile = $form->get('logoUrl')->getData();
|
||||||
|
if ($logoFile) {
|
||||||
|
$this->organizationsService->handleLogo($organization, $logoFile);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$this->entityManager->persist($organization);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->actionService->createAction("Edit Organization", $actingUser, $organization, $organization->getName());
|
||||||
|
return $this->redirectToRoute('organization_index');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->addFlash('error', 'Error editing organization: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this->render('organization/edit.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'organization' => $organization,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/view/{id}', name: 'show', methods: ['GET'])]
|
||||||
|
public function view($id): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
$organization = $this->organizationsRepository->find($id);
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
if (!$organization) {
|
||||||
|
$this->addFlash('error', self::NOT_FOUND);
|
||||||
|
return $this->redirectToRoute('organization_index');
|
||||||
|
}
|
||||||
|
//check if the user is admin of the organization
|
||||||
|
if (!$this->isGranted("ROLE_SUPER_ADMIN") && !$this->userService->isAdminOfOrganization($organization)) {
|
||||||
|
$this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$newUO = $this->entityManager->getRepository(UsersOrganizations::class)->findNewestUO($organization);
|
||||||
|
$newUsers = [];
|
||||||
|
foreach ($newUO as $uo) {
|
||||||
|
$newUsers[] = $uo->getUsers();
|
||||||
|
}
|
||||||
|
$adminUO = $this->entityManager->getRepository(UsersOrganizations::class)->findAdminsInOrganization($organization);
|
||||||
|
$adminUsers = [];
|
||||||
|
foreach ($adminUO as $uo) {
|
||||||
|
$adminUsers[] = $uo->getUsers();
|
||||||
|
}
|
||||||
|
$uos = $this->entityManager
|
||||||
|
->getRepository(UsersOrganizations::class)
|
||||||
|
->findBy(['organization' => $organization]);
|
||||||
|
|
||||||
|
$users = $this->userService->formatOrgUsers($uos);
|
||||||
|
|
||||||
|
$allApps = $this->entityManager->getRepository(Apps::class)->findAll(); // appsAll
|
||||||
|
$orgApps = $organization->getApps()->toArray(); // apps
|
||||||
|
|
||||||
|
$apps = $this->organizationsService->appsAccess($allApps, $orgApps);
|
||||||
|
|
||||||
|
$actions = $this->entityManager->getRepository(Actions::class)->findBy(['Organization' => $organization], limit: 15);
|
||||||
|
$activities = $this->actionService->formatActivities($actions);
|
||||||
|
|
||||||
|
$this->actionService->createAction("View Organization", $actingUser, $organization, $organization->getName());
|
||||||
|
return $this->render('organization/show.html.twig', [
|
||||||
|
'organization' => $organization,
|
||||||
|
'newUsers' => $newUsers,
|
||||||
|
'adminUsers' => $adminUsers,
|
||||||
|
'users' => $users,
|
||||||
|
'applications' => $apps,
|
||||||
|
'activities' => $activities,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/delete/{id}', name: 'delete', methods: ['POST'])]
|
||||||
|
public function delete($id): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted("ROLE_ADMIN");
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
$organization = $this->organizationsRepository->find($id);
|
||||||
|
if (!$organization) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$organization->setIsActive(false);
|
||||||
|
$organization->setIsDeleted(true);
|
||||||
|
// Deactivate all associated UsersOrganizations
|
||||||
|
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
||||||
|
|
||||||
|
$this->entityManager->persist($organization);
|
||||||
|
$this->actionService->createAction("Delete Organization", $actingUser, $organization, $organization->getName());
|
||||||
|
return $this->redirectToRoute('organization_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/deactivate/{id}', name: 'deactivate', methods: ['POST'])]
|
||||||
|
public function deactivate($id): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
$organization = $this->organizationsRepository->find($id);
|
||||||
|
if (!$organization) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$organization->setIsActive(false);
|
||||||
|
// $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
||||||
|
$this->entityManager->persist($organization);
|
||||||
|
$this->actionService->createAction("Deactivate Organization", $actingUser, $organization, $organization->getName());
|
||||||
|
return $this->redirectToRoute('organization_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/activate/{id}', name: 'activate', methods: ['POST'])]
|
||||||
|
public function activate($id): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
$organization = $this->organizationsRepository->find($id);
|
||||||
|
if (!$organization) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$organization->setIsActive(true);
|
||||||
|
$this->entityManager->persist($organization);
|
||||||
|
$this->actionService->createAction("Activate Organization", $actingUser, $organization, $organization->getName());
|
||||||
|
return $this->redirectToRoute('organization_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
// API endpoint to fetch organization data for Tabulator
|
||||||
|
#[Route(path: '/data', name: 'data', methods: ['GET'])]
|
||||||
|
public function data(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
|
||||||
|
$page = max(1, (int)$request->query->get('page', 1));
|
||||||
|
$size = max(1, (int)$request->query->get('size', 10));
|
||||||
|
|
||||||
|
$filters = $request->query->all('filter');
|
||||||
|
|
||||||
|
|
||||||
|
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
$qb = $this->organizationsRepository->createQueryBuilder('o')
|
||||||
|
->where('o.isDeleted = :del')->setParameter('del', false);
|
||||||
|
|
||||||
|
if (!empty($filters['name'])) {
|
||||||
|
$qb->andWhere('o.name LIKE :name')
|
||||||
|
->setParameter('name', '%' . $filters['name'] . '%');
|
||||||
|
}
|
||||||
|
if (!empty($filters['email'])) {
|
||||||
|
$qb->andWhere('o.email LIKE :email')
|
||||||
|
->setParameter('email', '%' . $filters['email'] . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count total
|
||||||
|
$countQb = clone $qb;
|
||||||
|
$total = (int)$countQb->select('COUNT(o.id)')->getQuery()->getSingleScalarResult();
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
$offset = ($page - 1) * $size;
|
||||||
|
$rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult();
|
||||||
|
|
||||||
|
// Map to array
|
||||||
|
$data = array_map(function (Organizations $org) {
|
||||||
|
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $org->getLogoUrl();
|
||||||
|
return [
|
||||||
|
'id' => $org->getId(),
|
||||||
|
'name' => $org->getName(),
|
||||||
|
'email' => $org->getEmail(),
|
||||||
|
'logoUrl' => $picture ?: null,
|
||||||
|
'active' => $org->isActive(),
|
||||||
|
'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]),
|
||||||
|
];
|
||||||
|
}, $rows);
|
||||||
|
|
||||||
|
// Tabulator expects: data, last_page (total pages), or total row count depending on config
|
||||||
|
$lastPage = (int)ceil($total / $size);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'data' => $data,
|
||||||
|
'last_page' => $lastPage,
|
||||||
|
'total' => $total, // optional, useful for debugging
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,12 @@
|
||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use App\Repository\UsersOrganizationsRepository;
|
||||||
use App\Service\AccessTokenService;
|
use App\Service\AccessTokenService;
|
||||||
|
use App\Service\OrganizationsService;
|
||||||
|
use App\Service\UserService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Psr\Log\LogLevel;
|
use Psr\Log\LogLevel;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
|
@ -16,9 +21,16 @@ use App\Service\CguUserService;
|
||||||
|
|
||||||
class SecurityController extends AbstractController
|
class SecurityController extends AbstractController
|
||||||
{
|
{
|
||||||
|
const NOT_FOUND = "NOT FOUND";
|
||||||
private CguUserService $cguUserService;
|
private CguUserService $cguUserService;
|
||||||
|
|
||||||
public function __construct(CguUserService $cguUserService)
|
public function __construct(CguUserService $cguUserService,
|
||||||
|
private readonly UserRepository $userRepository,
|
||||||
|
private readonly UserService $userService,
|
||||||
|
private readonly UsersOrganizationsRepository $uoRepository,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly OrganizationsService $organizationsService)
|
||||||
{
|
{
|
||||||
$this->cguUserService = $cguUserService;
|
$this->cguUserService = $cguUserService;
|
||||||
}
|
}
|
||||||
|
|
@ -26,14 +38,9 @@ class SecurityController extends AbstractController
|
||||||
#[Route(path: '/login', name: 'app_login')]
|
#[Route(path: '/login', name: 'app_login')]
|
||||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||||
{
|
{
|
||||||
// get the login error if there is one
|
|
||||||
$error = $authenticationUtils->getLastAuthenticationError();
|
$error = $authenticationUtils->getLastAuthenticationError();
|
||||||
|
|
||||||
// last username entered by the user
|
|
||||||
$lastUsername = $authenticationUtils->getLastUsername();
|
$lastUsername = $authenticationUtils->getLastUsername();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return $this->render('security/login.html.twig', [
|
return $this->render('security/login.html.twig', [
|
||||||
'last_username' => $lastUsername,
|
'last_username' => $lastUsername,
|
||||||
'error' => $error,
|
'error' => $error,
|
||||||
|
|
@ -43,39 +50,88 @@ class SecurityController extends AbstractController
|
||||||
#[Route(path: '/sso_logout', name: 'sso_logout')]
|
#[Route(path: '/sso_logout', name: 'sso_logout')]
|
||||||
public function ssoLogout(RequestStack $stack, LoggerInterface $logger, AccessTokenService $accessTokenService, Security $security): Response
|
public function ssoLogout(RequestStack $stack, LoggerInterface $logger, AccessTokenService $accessTokenService, Security $security): Response
|
||||||
{
|
{
|
||||||
// Invalidate the session and revoke tokens
|
|
||||||
try{
|
try{
|
||||||
if( $stack->getSession()->invalidate()){
|
if( $stack->getSession()->invalidate()){
|
||||||
$accessTokenService->revokeTokens($security->getUser()->getUserIdentifier());
|
$accessTokenService->revokeTokens($security->getUser()->getUserIdentifier());
|
||||||
$security->logout(false);
|
$security->logout(false);
|
||||||
$logger->info("Logout successfully");
|
$logger->info("Logout successfully");
|
||||||
// Redirect back to the client (or to a “you are logged out” page)
|
|
||||||
return $this->redirect('/');
|
return $this->redirect('/');
|
||||||
}
|
}
|
||||||
}catch (\Exception $e){
|
}catch (\Exception $e){
|
||||||
$logger->log(LogLevel::ERROR, 'Error invalidating session: ' . $e->getMessage());
|
$logger->log(LogLevel::ERROR, 'Error invalidating session: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
// If something goes wrong, redirect to the index page
|
|
||||||
return $this->redirectToRoute('app_index');
|
return $this->redirectToRoute('app_index');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/consent', name: 'app_consent')]
|
#[Route(path: '/consent', name: 'app_consent')]
|
||||||
public function consent(Request $request): Response
|
public function consent(Request $request): Response
|
||||||
{
|
{
|
||||||
// Handle form submission
|
|
||||||
if ($request->isMethod('POST')) {
|
if ($request->isMethod('POST')) {
|
||||||
// Check if user declined consent
|
|
||||||
if (!$request->request->has('decline')) {
|
if (!$request->request->has('decline')) {
|
||||||
// User accepted the CGU, save this in the database
|
|
||||||
$this->cguUserService->acceptLatestCgu($this->getUser());
|
$this->cguUserService->acceptLatestCgu($this->getUser());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back to the OAuth authorization endpoint with all the query parameters
|
|
||||||
return $this->redirectToRoute('oauth2_authorize', $request->query->all());
|
return $this->redirectToRoute('oauth2_authorize', $request->query->all());
|
||||||
}
|
}
|
||||||
|
|
||||||
// For GET requests, just show the consent form
|
|
||||||
return $this->render('security/consent.html.twig');
|
return $this->render('security/consent.html.twig');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/password_setup/{id}', name: 'password_setup', methods: ['GET'])]
|
||||||
|
public function password_setup(int $id, Request $request): Response
|
||||||
|
{
|
||||||
|
$error = $request->get('error');
|
||||||
|
$user = $this->userRepository->find($id);
|
||||||
|
if (!$user) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$token = $request->get('token');
|
||||||
|
if (empty($token) || !$this->userService->isPasswordTokenValid($user, $token)) {
|
||||||
|
$error = 'Le lien de définition du mot de passe est invalide ou a expiré. Veuillez en demander un nouveau.';
|
||||||
|
$this->logger->warning($user->getUserIdentifier(). " tried to use an invalid or expired password setup token.");
|
||||||
|
}
|
||||||
|
return $this->render('security/password_setup.html.twig', [
|
||||||
|
'id' => $id,
|
||||||
|
'token' => $token,
|
||||||
|
'error' => $error,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/password_reset/{id}', name: 'password_reset', methods: ['POST'])]
|
||||||
|
public function password_reset(int $id): Response
|
||||||
|
{
|
||||||
|
$user = $this->userRepository->find($id);
|
||||||
|
if (!$user) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$newPassword = $_POST['_password'] ?? null;
|
||||||
|
$confirmPassword = $_POST['_passwordConfirm'] ?? null;
|
||||||
|
if ($newPassword !== $confirmPassword) {
|
||||||
|
$error = 'Les mots de passe ne correspondent pas. Veuillez réessayer.';
|
||||||
|
$this->logger->warning($user->getUserIdentifier(). " provided non-matching passwords during password reset.");
|
||||||
|
return $this->redirectToRoute('password_setup', [
|
||||||
|
'id' => $id,
|
||||||
|
'token' => $_POST['token'] ?? '',
|
||||||
|
'error'=> $error]);
|
||||||
|
}
|
||||||
|
if (!$this->userService->isPasswordStrong($newPassword)) {
|
||||||
|
$error = 'Le mot de passe ne respecte pas les critères de sécurité. Veuillez en choisir un autre.';
|
||||||
|
$this->logger->warning($user->getUserIdentifier(). " provided a weak password during password reset.");
|
||||||
|
return $this->redirectToRoute('password_setup', ['id' => $id, 'token' => $_POST['token'] ?? '', 'error'=> $error]);
|
||||||
|
}
|
||||||
|
$this->userService->updateUserPassword($user, $newPassword);
|
||||||
|
$orgId = $this->userService->getOrgFromToken( $_POST['token']);
|
||||||
|
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
|
||||||
|
if($uo){
|
||||||
|
$uo->setStatut("ACCEPTED");
|
||||||
|
$uo->setIsActive(true);
|
||||||
|
$this->entityManager->persist($uo);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$data = ['user' => $user, 'organization' => $uo->getOrganization()];
|
||||||
|
|
||||||
|
$this->organizationsService->notifyOrganizationAdmins($data, "USER_ACCEPTED");
|
||||||
|
}
|
||||||
|
return $this->redirectToRoute('app_index');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,738 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Apps;
|
||||||
|
use App\Entity\Roles;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Entity\UserOrganizatonApp;
|
||||||
|
use App\Entity\UsersOrganizations;
|
||||||
|
use App\Form\UserForm;
|
||||||
|
use App\Repository\AppsRepository;
|
||||||
|
use App\Repository\OrganizationsRepository;
|
||||||
|
use App\Repository\RolesRepository;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use App\Repository\UsersOrganizationsRepository;
|
||||||
|
use App\Service\ActionService;
|
||||||
|
use App\Service\AwsService;
|
||||||
|
use App\Service\EmailService;
|
||||||
|
use App\Service\OrganizationsService;
|
||||||
|
use App\Service\UserOrganizationAppService;
|
||||||
|
use App\Service\UserOrganizationService;
|
||||||
|
use App\Service\UserService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use mysql_xdevapi\Exception;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Symfony\Component\Mailer\Mailer;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route(path: '/user', name: 'user_')]
|
||||||
|
class UserController extends AbstractController
|
||||||
|
{
|
||||||
|
private const NOT_FOUND = 'Entity not found';
|
||||||
|
private const ACCESS_DENIED = 'Access denied';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly UserService $userService,
|
||||||
|
private readonly ActionService $actionService,
|
||||||
|
private readonly UserOrganizationAppService $userOrganizationAppService,
|
||||||
|
private readonly UserOrganizationService $userOrganizationService,
|
||||||
|
private readonly UserRepository $userRepository,
|
||||||
|
private readonly UsersOrganizationsRepository $uoRepository,
|
||||||
|
private readonly OrganizationsRepository $organizationRepository,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly EmailService $emailService,
|
||||||
|
private readonly AwsService $awsService,
|
||||||
|
private readonly OrganizationsService $organizationsService,
|
||||||
|
private readonly AppsRepository $appsRepository,
|
||||||
|
private readonly RolesRepository $rolesRepository,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[Route('/view/{id}', name: 'show', methods: ['GET'])]
|
||||||
|
public function view(int $id, Request $request): Response
|
||||||
|
{
|
||||||
|
// Accès : uniquement utilisateur authentifié
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||||
|
|
||||||
|
// Utilisateur courant (acting user) via UserService
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
// Vérification des droits d'accès supplémentaires
|
||||||
|
if (!$this->userService->hasAccessTo($actingUser)) {
|
||||||
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chargement de l'utilisateur cible à afficher
|
||||||
|
$user = $this->userRepository->find($id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Paramètre optionnel de contexte organisationnel
|
||||||
|
$orgId = $request->query->get('organizationId');
|
||||||
|
|
||||||
|
// Liste de toutes les applications (pour créer des groupes même si vides)
|
||||||
|
$apps = $this->appsRepository->findAll();
|
||||||
|
|
||||||
|
// Initialisations pour la résolution des UsersOrganizations (UO)
|
||||||
|
$singleUo = null;
|
||||||
|
$uoActive = null;
|
||||||
|
|
||||||
|
// get uo or uoS based on orgId
|
||||||
|
if ($orgId) {
|
||||||
|
// Contexte organisation précis : récupérer l'organisation et les liens UO
|
||||||
|
$organization = $this->organizationRepository->findBy(['id' => $orgId]);
|
||||||
|
$uoList = $this->uoRepository->findBy([
|
||||||
|
'users' => $user,
|
||||||
|
'organization' => $organization,
|
||||||
|
'isActive' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$uoList) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si contexte org donné, on retient la première UO (singleUo)
|
||||||
|
$singleUo = $uoList[0];
|
||||||
|
$data["singleUo"] = $singleUo;
|
||||||
|
$uoActive = $singleUo->isActive();
|
||||||
|
} else {
|
||||||
|
// Pas de contexte org : récupérer toutes les UO actives de l'utilisateur
|
||||||
|
$uoList = $this->uoRepository->findBy([
|
||||||
|
'users' => $user,
|
||||||
|
'isActive' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées
|
||||||
|
// Load user-organization-app roles (can be empty)
|
||||||
|
$uoa = $this->entityManager
|
||||||
|
->getRepository(UserOrganizatonApp::class)
|
||||||
|
->findBy([
|
||||||
|
'userOrganization' => $uoList,
|
||||||
|
'isActive' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Group UOA by app and ensure every app has a group
|
||||||
|
$data['uoas'] = $this->userOrganizationAppService
|
||||||
|
->groupUserOrganizationAppsByApplication(
|
||||||
|
$uoa,
|
||||||
|
$apps,
|
||||||
|
$singleUo ? $singleUo->getId() : null
|
||||||
|
);
|
||||||
|
|
||||||
|
//Build roles based on user permissions.
|
||||||
|
//Admin can't see or edit a super admin user
|
||||||
|
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||||
|
$data['rolesArray'] = $this->rolesRepository->findAll();
|
||||||
|
} elseif (!$orgId) {
|
||||||
|
$data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser, true);
|
||||||
|
} else {
|
||||||
|
$data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Calcul du flag de modification : utilisateur admin ET exactement 1 UO
|
||||||
|
$canEdit = $this->userService->canEditRolesCheck($actingUser, $user, $organization, $this->isGranted('ROLE_ADMIN'));
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// En cas d'erreur, désactiver l'édition et logger l'exception
|
||||||
|
$canEdit = false;
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
}
|
||||||
|
return $this->render('user/show.html.twig', [
|
||||||
|
'user' => $user,
|
||||||
|
'organizationId' => $orgId ?? null,
|
||||||
|
'uoActive' => $uoActive ?? null,
|
||||||
|
'apps' => $apps ?? [],
|
||||||
|
'data' => $data ?? [],
|
||||||
|
'canEdit' => $canEdit ?? false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
|
||||||
|
public function edit(int $id, Request $request): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
if ($this->userService->hasAccessTo($actingUser)) {
|
||||||
|
$user = $this->userRepository->find($id);
|
||||||
|
if (!$user) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$form = $this->createForm(UserForm::class, $user);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
// Handle file upload
|
||||||
|
$picture = $form->get('pictureUrl')->getData();
|
||||||
|
|
||||||
|
if ($picture) {
|
||||||
|
$this->userService->handleProfilePicture($user, $picture);
|
||||||
|
}
|
||||||
|
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
|
$this->entityManager->persist($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
//log and action
|
||||||
|
$this->logger->notice("User information edited for " . $user->getUserIdentifier());
|
||||||
|
if ($request->get('organizationId')) {
|
||||||
|
$org = $this->organizationRepository->find($request->get('organizationId'));
|
||||||
|
if ($org) {
|
||||||
|
$this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier());
|
||||||
|
return $this->redirectToRoute('user_show', ['id' => $user->getId(), 'organizationId' => $request->get('organizationId')]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->actionService->createAction("Edit user information", $actingUser, null, $user->getUserIdentifier());
|
||||||
|
return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('user/edit.html.twig', [
|
||||||
|
'user' => $user,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'organizationId' => $request->get('organizationId')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
|
||||||
|
public function new(Request $request): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
try {
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
if ($this->userService->hasAccessTo($actingUser)) {
|
||||||
|
$user = new User();
|
||||||
|
$form = $this->createForm(UserForm::class, $user);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
$orgId = $request->get('organizationId');
|
||||||
|
if ($orgId){
|
||||||
|
$org = $this->organizationRepository->find($orgId) ?? throw new NotFoundHttpException(sprintf('%s not found', $orgId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
|
||||||
|
if ($existingUser && $orgId) {
|
||||||
|
$this->userService->handleExistingUser($existingUser, $org);
|
||||||
|
|
||||||
|
$this->actionService->createAction("Create new user", $existingUser, $org, "Added user to organization" . $existingUser->getUserIdentifier() . " for organization " . $org->getName());
|
||||||
|
$this->logger->notice("User added to organization " . $org->getName());
|
||||||
|
$this->emailService->sendExistingUserNotificationEmail($existingUser, $org);
|
||||||
|
$this->logger->notice("Existing user notification email sent to " . $existingUser->getUserIdentifier());
|
||||||
|
$data = ['user' => $existingUser, 'organization' => $org];
|
||||||
|
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
|
||||||
|
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Handle file upload
|
||||||
|
$picture = $form->get('pictureUrl')->getData();
|
||||||
|
$this->userService->formatNewUserData($user, $picture);
|
||||||
|
|
||||||
|
if ($orgId) {
|
||||||
|
$uo = new UsersOrganizations();
|
||||||
|
$uo->setUsers($user);
|
||||||
|
$uo->setOrganization($org);
|
||||||
|
$uo->setStatut("INVITED");
|
||||||
|
$uo->setIsActive(false);
|
||||||
|
$uo->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
|
$this->entityManager->persist($uo);
|
||||||
|
$this->actionService->createAction("Create new user", $user, $org, "Added user to organization" . $user->getUserIdentifier() . " for organization " . $org->getName());
|
||||||
|
$this->logger->notice("User added to organization " . $org->getName());
|
||||||
|
$this->emailService->sendPasswordSetupEmail($user, $orgId);
|
||||||
|
$this->logger->notice("Password setup email sent to " . $user->getUserIdentifier());
|
||||||
|
$data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()];
|
||||||
|
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
|
||||||
|
}
|
||||||
|
$this->actionService->createAction("Create new user", $actingUser, null, $user->getUserIdentifier());
|
||||||
|
$this->logger->notice("User created " . $user->getUserIdentifier());
|
||||||
|
$this->entityManager->persist($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
if ($orgId) {
|
||||||
|
return $this->redirectToRoute('organization_show', ['organizationId' => $orgId]);
|
||||||
|
}
|
||||||
|
return $this->redirectToRoute('user_index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this->render('user/new.html.twig', [
|
||||||
|
'user' => $user,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'organizationId' => $orgId
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
if ($orgId) {
|
||||||
|
return $this->redirectToRoute('organization_show', ['id' => $orgId]);
|
||||||
|
}
|
||||||
|
return $this->redirectToRoute('user_index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[Route('/activeStatus/{id}', name: 'active_status', methods: ['GET', 'POST'])]
|
||||||
|
public function activeStatus(int $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
try{
|
||||||
|
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||||
|
$user = $this->userRepository->find($id);
|
||||||
|
if (!$user) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$status = $request->get('status');
|
||||||
|
if ($status === 'deactivate') {
|
||||||
|
$user->setIsActive(false);
|
||||||
|
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
||||||
|
if ($this->userService->isUserConnected($user->getUserIdentifier())) {
|
||||||
|
$this->userService->revokeUserTokens($user->getUserIdentifier());
|
||||||
|
}
|
||||||
|
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
|
$this->entityManager->persist($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->logger->notice("User deactivated " . $user->getUserIdentifier());
|
||||||
|
$this->actionService->createAction("Deactivate user", $actingUser, null, $user->getUserIdentifier());
|
||||||
|
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status === 'activate') {
|
||||||
|
$user->setIsActive(true);
|
||||||
|
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
|
$this->logger->notice("User activated " . $user->getUserIdentifier());
|
||||||
|
$this->actionService->createAction("Activate user", $actingUser, null, $user->getUserIdentifier());
|
||||||
|
return new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (\Exception $e){
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
}
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/organization/activateStatus/{id}', name: 'activate_organization', methods: ['GET', 'POST'])]
|
||||||
|
public function activateStatusOrganization(int $id, Request $request): JsonResponse{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
try {
|
||||||
|
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||||
|
$orgId = $request->get('organizationId');
|
||||||
|
$org = $this->organizationRepository->find($orgId);
|
||||||
|
if (!$org) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$user = $this->userRepository->find($id);
|
||||||
|
if (!$user) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
||||||
|
'organization' => $org]);
|
||||||
|
if (!$uo) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$status = $request->get('status');
|
||||||
|
if ($status === 'deactivate') {
|
||||||
|
$uo->setIsActive(false);
|
||||||
|
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo);
|
||||||
|
$this->entityManager->persist($uo);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$data = ['user' => $user,
|
||||||
|
'organization' => $org];
|
||||||
|
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DEACTIVATED");
|
||||||
|
$this->logger->notice("User Organizaton deactivated " . $user->getUserIdentifier());
|
||||||
|
$this->actionService->createAction("Deactivate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
|
||||||
|
return new JsonResponse(['status' => 'deactivated'], Response::HTTP_OK);
|
||||||
|
}
|
||||||
|
if($status === "activate"){
|
||||||
|
$uo->setIsActive(true);
|
||||||
|
$this->entityManager->persist($uo);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->actionService->createAction("Activate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
|
||||||
|
$data = ['user' => $user,
|
||||||
|
'organization' => $org];
|
||||||
|
$this->organizationsService->notifyOrganizationAdmins($data, "USER_ACTIVATED");
|
||||||
|
return new JsonResponse(['status' => 'activated'], Response::HTTP_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (\Exception $exception){
|
||||||
|
$this->logger->error($exception->getMessage());
|
||||||
|
}
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO : MONOLOG + remove picture from bucket
|
||||||
|
#[Route('/delete/{id}', name: 'delete', methods: ['GET', 'POST'])]
|
||||||
|
public function delete(int $id, Request $request): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
$user = $this->userRepository->find($id);
|
||||||
|
if (!$user) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$user->setIsActive(false);
|
||||||
|
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
|
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
||||||
|
$user->setIsDeleted(true);
|
||||||
|
if ($this->userService->isUserConnected($user)) {
|
||||||
|
$this->userService->revokeUserTokens($user->getUserIdentifier());
|
||||||
|
}
|
||||||
|
$this->entityManager->persist($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->actionService->createAction("Delete user", $actingUser, null, $user->getUserIdentifier());
|
||||||
|
$data = ['user' => $user,
|
||||||
|
'organization' => null];
|
||||||
|
$this->organizationsService->notifyOrganizationAdmins($data, "USER_DELETED");
|
||||||
|
|
||||||
|
return new Response('', Response::HTTP_NO_CONTENT); //204
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO : MONOLOG
|
||||||
|
#[Route(path: '/application/roles/{id}', name: 'application_role', methods: ['GET', 'POST'])]
|
||||||
|
public function applicationRole(int $id, Request $request): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted("ROLE_ADMIN");
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||||
|
$uo = $this->userOrganizationService->getByIdOrFail($id);
|
||||||
|
$application = $this->entityManager->getRepository(Apps::class)->find($request->get('appId'));
|
||||||
|
if (!$application) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedRolesIds = $request->get('roles', []);
|
||||||
|
$roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']);
|
||||||
|
if (!$roleUser) {
|
||||||
|
throw $this->createNotFoundException('Default role not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($selectedRolesIds)) {
|
||||||
|
if (!in_array((string)$roleUser->getId(), $selectedRolesIds, true)) {
|
||||||
|
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application);
|
||||||
|
} else {
|
||||||
|
$this->userOrganizationAppService->syncRolesForUserOrganizationApp(
|
||||||
|
$uo,
|
||||||
|
$application,
|
||||||
|
$selectedRolesIds,
|
||||||
|
$actingUser
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $uo->getUsers();
|
||||||
|
return $this->redirectToRoute('user_show', [
|
||||||
|
'user' => $user,
|
||||||
|
'id' => $user->getId(),
|
||||||
|
'organizationId' => $uo->getOrganization()->getId()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AJAX endpoint for user listing with pagination
|
||||||
|
* Get all the users that aren´t deleted and are active
|
||||||
|
*/
|
||||||
|
#[Route(path: '/data', name: 'data', methods: ['GET'])]
|
||||||
|
public function data(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted("ROLE_ADMIN");
|
||||||
|
|
||||||
|
$page = max(1, (int)$request->query->get('page', 1));
|
||||||
|
$size = max(1, (int)$request->query->get('size', 10));
|
||||||
|
|
||||||
|
// Get filter parameters
|
||||||
|
$filters = $request->query->all('filter', []);
|
||||||
|
|
||||||
|
$repo = $this->userRepository;
|
||||||
|
|
||||||
|
// Base query
|
||||||
|
$qb = $repo->createQueryBuilder('u')
|
||||||
|
->where('u.isDeleted = :del')->setParameter('del', false);
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (!empty($filters['name'])) {
|
||||||
|
$qb->andWhere('u.surname LIKE :name')
|
||||||
|
->setParameter('name', '%' . $filters['name'] . '%');
|
||||||
|
}
|
||||||
|
if (!empty($filters['prenom'])) {
|
||||||
|
$qb->andWhere('u.name LIKE :prenom')
|
||||||
|
->setParameter('prenom', '%' . $filters['prenom'] . '%');
|
||||||
|
}
|
||||||
|
if (!empty($filters['email'])) {
|
||||||
|
$qb->andWhere('u.email LIKE :email')
|
||||||
|
->setParameter('email', '%' . $filters['email'] . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
$countQb = clone $qb;
|
||||||
|
$total = (int)$countQb->select('COUNT(u.id)')->getQuery()->getSingleScalarResult();
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
$offset = ($page - 1) * $size;
|
||||||
|
$rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult();
|
||||||
|
|
||||||
|
// Map to array
|
||||||
|
$data = array_map(function (User $user) {
|
||||||
|
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl();
|
||||||
|
return [
|
||||||
|
'id' => $user->getId(),
|
||||||
|
'pictureUrl' => $picture,
|
||||||
|
'name' => $user->getSurname(),
|
||||||
|
'prenom' => $user->getName(),
|
||||||
|
'email' => $user->getEmail(),
|
||||||
|
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
||||||
|
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
||||||
|
'statut' => $user->isActive(),
|
||||||
|
];
|
||||||
|
}, $rows);
|
||||||
|
|
||||||
|
$lastPage = (int)ceil($total / $size);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'data' => $data,
|
||||||
|
'last_page' => $lastPage,
|
||||||
|
'total' => $total,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/', name: 'index', methods: ['GET'])]
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||||
|
$totalUsers = $this->userRepository->count(['isDeleted' => false, 'isActive' => true]);
|
||||||
|
return $this->render('user/index.html.twig', [
|
||||||
|
'users' => $totalUsers
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AJAX endpoint for new users listing
|
||||||
|
* Get the 5 most recently created users for an organization
|
||||||
|
*/
|
||||||
|
#[Route(path: '/data/new', name: 'dataNew', methods: ['GET'])]
|
||||||
|
public function dataNew(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||||
|
$orgId = $request->query->get('orgId');
|
||||||
|
$uos = $this->uoRepository->findBy(['organization' => $orgId, 'statut' => ["ACCEPTED", "INVITED"]],
|
||||||
|
orderBy: ['createdAt' => 'DESC'], limit: 5);
|
||||||
|
|
||||||
|
|
||||||
|
// Map to array (keep isConnected)
|
||||||
|
$data = array_map(function (UsersOrganizations $uo) {
|
||||||
|
$user = $uo->getUsers();
|
||||||
|
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl();
|
||||||
|
$initials = $user->getName()[0] . $user->getSurname()[0];
|
||||||
|
return [
|
||||||
|
'pictureUrl' => $picture,
|
||||||
|
'email' => $user->getEmail(),
|
||||||
|
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
||||||
|
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
||||||
|
'initials' => strtoupper($initials),
|
||||||
|
];
|
||||||
|
}, $uos);
|
||||||
|
return $this->json([
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AJAX endpoint for admin users listing
|
||||||
|
* Get all admin users for an organization
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[Route(path: '/data/admin', name: 'dataAdmin', methods: ['GET'])]
|
||||||
|
public function dataAdmin(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||||
|
$orgId = $request->query->get('orgId');
|
||||||
|
$uos = $this->uoRepository->findBy(['organization' => $orgId]);
|
||||||
|
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||||
|
$users = [];
|
||||||
|
foreach ($uos as $uo) {
|
||||||
|
if ($this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin])) {
|
||||||
|
$users[] = $uo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Map to array (keep isConnected)
|
||||||
|
$data = array_map(function (UsersOrganizations $uo) {
|
||||||
|
$user = $uo->getUsers();
|
||||||
|
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl();
|
||||||
|
$initials = $user->getName()[0] . $user->getSurname()[0];
|
||||||
|
return [
|
||||||
|
'pictureUrl' => $picture,
|
||||||
|
'email' => $user->getEmail(),
|
||||||
|
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
||||||
|
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
||||||
|
'initials' => strtoupper($initials),
|
||||||
|
];
|
||||||
|
}, $users);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AJAX endpoint for All users in an organization
|
||||||
|
*/
|
||||||
|
#[Route(path: '/data/organization', name: 'dataUserOrganization', methods: ['GET'])]
|
||||||
|
public function dataUserOrganization(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
|
||||||
|
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||||
|
$orgId = $request->query->get('orgId');
|
||||||
|
$page = max(1, (int)$request->query->get('page', 1));
|
||||||
|
$size = max(1, (int)$request->query->get('size', 10));
|
||||||
|
|
||||||
|
$filters = $request->query->all('filter') ?? [];
|
||||||
|
|
||||||
|
$repo = $this->uoRepository;
|
||||||
|
|
||||||
|
// Base query
|
||||||
|
$qb = $repo->createQueryBuilder('uo')
|
||||||
|
->join('uo.users', 'u')
|
||||||
|
->where('uo.organization = :orgId')
|
||||||
|
->setParameter('orgId', $orgId);
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (!empty($filters['name'])) {
|
||||||
|
$qb->andWhere('u.surname LIKE :name')
|
||||||
|
->setParameter('name', '%' . $filters['name'] . '%');
|
||||||
|
}
|
||||||
|
if (!empty($filters['prenom'])) {
|
||||||
|
$qb->andWhere('u.name LIKE :prenom')
|
||||||
|
->setParameter('prenom', '%' . $filters['prenom'] . '%');
|
||||||
|
}
|
||||||
|
if (!empty($filters['email'])) {
|
||||||
|
$qb->andWhere('u.email LIKE :email')
|
||||||
|
->setParameter('email', '%' . $filters['email'] . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
$countQb = clone $qb;
|
||||||
|
$total = (int)$countQb->select('COUNT(uo.id)')->getQuery()->getSingleScalarResult();
|
||||||
|
|
||||||
|
$qb->orderBy('uo.isActive', 'DESC')
|
||||||
|
->addOrderBy('CASE WHEN uo.statut = :invited THEN 0 ELSE 1 END', 'ASC')
|
||||||
|
->setParameter('invited', 'INVITED');
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $size;
|
||||||
|
$rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult();
|
||||||
|
$data = $this->userService->formatStatutForOrganizations($rows);
|
||||||
|
|
||||||
|
$lastPage = (int)ceil($total / $size);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'data' => $data,
|
||||||
|
'last_page' => $lastPage,
|
||||||
|
'total' => $total,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/organization/resend-invitation/{userId}', name: 'resend_invitation', methods: ['POST'])]
|
||||||
|
public function resendInvitation(int $userId, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted("ROLE_ADMIN");
|
||||||
|
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||||
|
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||||
|
$orgId = $request->get('organizationId');
|
||||||
|
$org = $this->organizationRepository->find($orgId);
|
||||||
|
if (!$org) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$user = $this->userRepository->find($userId);
|
||||||
|
if (!$user) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
||||||
|
'organization' => $org,
|
||||||
|
'statut' => "INVITED"]);
|
||||||
|
if (!$uo) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
$uo->setModifiedAt(new \DateTimeImmutable());
|
||||||
|
try {
|
||||||
|
$data = ['user' => $uo->getUsers(), 'organization' => $uo->getOrganization()];
|
||||||
|
$this->emailService->sendPasswordSetupEmail($user, $orgId);
|
||||||
|
$this->logger->info("Invitation email resent to user " . $user->getUserIdentifier() . " for organization " . $org->getName());
|
||||||
|
$this->organizationsService->notifyOrganizationAdmins($data, 'USER_INVITED');
|
||||||
|
return $this->json(['message' => 'Invitation envoyée avec success.'], Response::HTTP_OK);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error("Error resending invitation email to user " . $user->getUserIdentifier() . " for organization " . $org->getName() . ": " . $e->getMessage());
|
||||||
|
return $this->json(['message' => 'Erreur lors de l\'envoie du mail.'], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/accept-invitation', name: 'accept', methods: ['GET'])]
|
||||||
|
public function acceptInvitation(Request $request): Response
|
||||||
|
{
|
||||||
|
$token = $request->get('token');
|
||||||
|
$userId = $request->get('id');
|
||||||
|
|
||||||
|
if (!$token || !$userId) {
|
||||||
|
throw $this->createNotFoundException('Invalid invitation link.');
|
||||||
|
}
|
||||||
|
$user = $this->userRepository->find($userId);
|
||||||
|
if (!$user) {
|
||||||
|
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (!$this->userService->isPasswordTokenValid($user, $token)) {
|
||||||
|
throw $this->createNotFoundException('Invalid or expired invitation token.');
|
||||||
|
}
|
||||||
|
$orgId = $this->userService->getOrgFromToken($token);
|
||||||
|
$uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $orgId]);
|
||||||
|
if (!$uo || $uo->getStatut() !== 'INVITED') {
|
||||||
|
$this->logger->warning("User " . $user->getUserIdentifier() . " tried to accept an invitation but no pending invitation was found for organization ID " . $orgId);
|
||||||
|
throw $this->createNotFoundException('No pending invitation found for this user and organization.');
|
||||||
|
}
|
||||||
|
$uo->setModifiedAt(new \DateTimeImmutable());
|
||||||
|
$uo->setStatut("ACCEPTED");
|
||||||
|
$uo->setIsActive(true);
|
||||||
|
$this->entityManager->persist($uo);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->logger->info("User " . $user->getUserIdentifier() . " accepted invitation for organization ID " . $orgId);
|
||||||
|
|
||||||
|
return $this->render('user/show.html.twig', ['user' => $user, 'orgId' => $orgId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||