Compare commits

...

15 Commits

Author SHA1 Message Date
Charles c3d3218bff update userForm 2025-07-17 11:45:02 +02:00
Charles f24fb0180d Edit user 2025-07-17 11:33:34 +02:00
Charles e360019e58 Type refacto 2025-07-17 11:32:28 +02:00
Charles 8e38fe47db Type refacto 2025-07-17 11:26:05 +02:00
Charles b54fe41795 const execption 2025-07-17 10:39:32 +02:00
Charles 798ca4ba07 unique email address on creation 2025-07-17 10:39:17 +02:00
Charles d43b516826 Create user 2025-07-17 09:16:09 +02:00
Charles c99b575814 Deactivate user 2025-07-16 15:59:56 +02:00
Charles 65ff838dd9 handle error 2025-07-16 15:50:45 +02:00
Charles 4a2f9d9547 Read User information 2025-07-16 15:12:45 +02:00
Charles 10a8eb2255 Correction 2025-07-16 15:04:35 +02:00
Charles 8f232498b8 Add phone number 2025-07-16 15:03:41 +02:00
Charles bbe50dbfd9 Last connection on user entity 2025-07-10 09:26:54 +02:00
Charles 436284c9c7 add name to Organizations entity 2025-07-09 14:11:31 +02:00
Charles 228ef8cbe9 refactor + update DB 2025-07-09 09:30:34 +02:00
40 changed files with 1147 additions and 226 deletions

View File

@ -141,7 +141,6 @@
<path value="$PROJECT_DIR$/vendor/defuse/php-encryption" />
<path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" />
<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/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,13 @@
/*variable*/
:root{
--primary-blue-light : #086572;
--primary-blue-dark : #094754;
--black-font: #1D1E1C;
--delete : #E42E31;
--disable : #A3A3A3;
--check : #80F20E;
}
html {
margin: 0;
padding: 0;
@ -77,4 +87,30 @@ body {
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);
}
.color-primary-dark{
color: var(--primary-blue-dark);
}

110
composer.lock generated
View File

@ -154,99 +154,6 @@
},
"time": "2023-06-19T06:10:36+00:00"
},
{
"name": "doctrine/cache",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
"reference": "1ca8f21980e770095a31456042471a57bc4c68fb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb",
"reference": "1ca8f21980e770095a31456042471a57bc4c68fb",
"shasum": ""
},
"require": {
"php": "~7.1 || ^8.0"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
"cache/integration-tests": "dev-master",
"doctrine/coding-standard": "^9",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"psr/cache": "^1.0 || ^2.0 || ^3.0",
"symfony/cache": "^4.4 || ^5.4 || ^6",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.",
"homepage": "https://www.doctrine-project.org/projects/cache.html",
"keywords": [
"abstraction",
"apcu",
"cache",
"caching",
"couchdb",
"memcached",
"php",
"redis",
"xcache"
],
"support": {
"issues": "https://github.com/doctrine/cache/issues",
"source": "https://github.com/doctrine/cache/tree/2.2.0"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache",
"type": "tidelift"
}
],
"time": "2022-05-20T20:07:39+00:00"
},
{
"name": "doctrine/collections",
"version": "2.3.0",
@ -335,28 +242,31 @@
},
{
"name": "doctrine/dbal",
"version": "3.9.5",
"version": "3.10.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "4a4e2eed3134036ee36a147ee0dac037dfa17868"
"reference": "1cf840d696373ea0d58ad0a8875c0fadcfc67214"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/4a4e2eed3134036ee36a147ee0dac037dfa17868",
"reference": "4a4e2eed3134036ee36a147ee0dac037dfa17868",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/1cf840d696373ea0d58ad0a8875c0fadcfc67214",
"reference": "1cf840d696373ea0d58ad0a8875c0fadcfc67214",
"shasum": ""
},
"require": {
"composer-runtime-api": "^2",
"doctrine/cache": "^1.11|^2.0",
"doctrine/deprecations": "^0.5.3|^1",
"doctrine/event-manager": "^1|^2",
"php": "^7.4 || ^8.0",
"psr/cache": "^1|^2|^3",
"psr/log": "^1|^2|^3"
},
"conflict": {
"doctrine/cache": "< 1.11"
},
"require-dev": {
"doctrine/cache": "^1.11|^2.0",
"doctrine/coding-standard": "13.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1",
@ -426,7 +336,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.9.5"
"source": "https://github.com/doctrine/dbal/tree/3.10.0"
},
"funding": [
{
@ -442,7 +352,7 @@
"type": "tidelift"
}
],
"time": "2025-06-15T22:40:05+00:00"
"time": "2025-07-10T21:11:04+00:00"
},
{
"name": "doctrine/deprecations",

View File

@ -11,8 +11,10 @@ security:
property: email
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
ROLE_SUDALYS: ROLE_USER
ROLE_ADMIN: ROLE_USER
ROLE_SUDALYS_ADMIN: [ROLE_SUDALYS, ROLE_ALLOWED_TO_SWITCH, ROLE_ADMIN]
firewalls:
dev:

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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)\'');
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -0,0 +1,178 @@
<?php
namespace App\Controller;
use App\Entity\User;
use App\Form\UserForm;
use App\Service\UserOrganizationService;
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: '/user', name: 'user_')]
class UserController extends AbstractController
{
private const NOT_FOUND = 'User not found';
public function __construct(
private readonly UserOrganizationService $userOrganizationService,
private readonly EntityManagerInterface $entityManager,
private readonly UserService $userService)
{
}
/**
* GET /user - List all users (index/collection)
*/
#[Route('/', name: 'index', methods: ['GET'])]
public function index(EntityManagerInterface $entityManager): Response
{
if ($this->isGranted('ROLE_SUDALYS_ADMIN')) {
$users = $entityManager->getRepository(User::class)->getAllActiveUsers();
} else {
$users = 'Not Super Admin';
}
return $this->render('user/index.html.twig', [
'users' => $users,
'controller_name' => 'IndexController',
]);
}
/**
* GET /user/{id} - Show specific user (show/member)
*/
#[Route('/{id}', name: 'show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function show(int $id, EntityManagerInterface $entityManager): Response
{
if (!$this->isGranted('ROLE_SUDALYS_ADMIN')) {
throw $this->createAccessDeniedException('Access denied');
}
$user = $entityManager->getRepository(User::class)->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$userOrganizations = $this->userOrganizationService->getUserOrganizations($user);
return $this->render('user/profile.html.twig', [
'user' => $user,
'userOrganizations' => $userOrganizations,
]);
}
/**
* GET /user/new - Show form to create new user and handle submission
*/
#[Route('/new', name: 'new', methods: ['GET', 'POST'])]
public function new(Request $request): Response
{
$form = $this->createForm(UserForm::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//Data is a User object. App\Form\NewUserForm is a form type that maps to User entity
$data = $form->getData();
// Handle user creation logic here
//FOR DEV PURPOSES ONLY
$data->setPictureUrl("");
$data->setPassword($this->userService->generateRandomPassword());
//FOR DEV PURPOSES ONLY
$this->entityManager->persist($data);
$this->entityManager->flush();
// Redirect to user index
return $this->redirectToRoute('user_index');
}
return $this->render('user/new.html.twig', [
'form' => $form->createView(),
]);
}
/**
* GET /user/{id}/edit - Show form to edit user
*/
#[Route('/{id}/edit', name: 'edit', requirements: ['id' => '\d+'], methods: ['GET', 'PUT', 'POST'])]
public function edit(int $id, EntityManagerInterface $entityManager, Request $request): Response
{
//Handle access control
if (!$this->isGranted('ROLE_SUDALYS_ADMIN')) {
throw $this->createAccessDeniedException('Access denied');
}
//Fetch user by ID and handle not found case
$user = $entityManager->getRepository(User::class)->find($id);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
//Create form for editing user
$form = $this->createForm(UserForm::class, $user);
//Handle form submission
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//Persist changes to the user entity
$entityManager->persist($user);
$entityManager->flush();
//Redirect to user profile after successful edit
return $this->redirectToRoute('user_show', ['id' => $user->getId()]);
}
return $this->render('user/edit.html.twig', [
'form' => $form->createView(),
'user' => $user,
]);
}
//
// /**
// * DELETE /user/{id} - Delete user
// */
// #[Route('/{id}', name: 'delete', methods: ['DELETE'], requirements: ['id' => '\d+'])]
// public function delete(int $id, EntityManagerInterface $entityManager): Response
// {
// if (!$this->isGranted('ROLE_SUDALYS_ADMIN')) {
// throw $this->createAccessDeniedException('Access denied');
// }
//
// $user = $entityManager->getRepository(User::class)->find($id);
// if (!$user) {
// throw $this->createNotFoundException(self::NOT_FOUND));
// }
//
// // Handle user deletion logic
// $entityManager->remove($user);
// $entityManager->flush();
//
// return $this->redirectToRoute('user_index');
// }
#[Route('/deactivate/{id}', name: 'deactivate', methods: ['GET'])]
public function userDeactivate(Request $request, EntityManagerInterface $entityManager): Response
{
if ($this->isGranted('ROLE_SUDALYS_ADMIN')) {
$userId = $request->attributes->get('id');
$user = $entityManager->getRepository(User::class)->find($userId);
if (!$user) {
throw $this->createNotFoundException(self::NOT_FOUND);
}
$user->setIsActive(false);
$entityManager->persist($user);
$entityManager->flush();
return $this->redirectToRoute('user_index');
}
return new Response('Unauthorized', Response::HTTP_UNAUTHORIZED);
}
}

View File

@ -42,6 +42,9 @@ class Organizations
#[ORM\ManyToMany(targetEntity: Apps::class, mappedBy: 'organization')]
private Collection $apps;
#[ORM\Column(length: 255, nullable: true)]
private ?string $name = null;
public function __construct()
{
$this->apps = new ArrayCollection();
@ -162,4 +165,16 @@ class Organizations
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
}

View File

@ -1,36 +0,0 @@
<?php
namespace App\Entity;
use App\Repository\SubscriptionsRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: SubscriptionsRepository::class)]
class Subscriptions
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'subscriptions')]
#[ORM\JoinColumn(nullable: false)]
private ?User $users = null;
public function getId(): ?int
{
return $this->id;
}
public function getUsers(): ?User
{
return $this->users;
}
public function setUsers(?User $users): static
{
$this->users = $users;
return $this;
}
}

View File

@ -5,10 +5,13 @@ namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
#[UniqueEntity(fields: ['email'], message: 'This email address is already in use.')]
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
@ -44,7 +47,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column(length: 255)]
private ?string $picture_url = null;
private ?string $pictureUrl = null;
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
private ?\DateTimeImmutable $modifiedAt = null;
@ -55,17 +58,21 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column(options: ['default' => false])]
private ?bool $isDeleted = null;
/**
* @var Collection<int, Subscriptions>
*/
#[ORM\OneToMany(targetEntity: Subscriptions::class, mappedBy: 'users')]
private Collection $subscriptions;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?\DateTime $lastConnection = null;
#[ORM\Column(length: 20, nullable: true)]
private ?string $phoneNumber = null;
public function __construct()
{
$this->subscriptions = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable();
$this->modifiedAt = new \DateTimeImmutable();
$this->isActive = true;
$this->isDeleted = false;
}
public function getId(): ?int
{
return $this->id;
@ -179,12 +186,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
public function getPictureUrl(): ?string
{
return $this->picture_url;
return $this->pictureUrl;
}
public function setPictureUrl(string $picture_url): static
public function setPictureUrl(string $pictureUrl): static
{
$this->picture_url = $picture_url;
$this->pictureUrl = $pictureUrl;
return $this;
}
@ -233,33 +240,29 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return (string) $this->getId();
}
/**
* @return Collection<int, Subscriptions>
*/
public function getSubscriptions(): Collection
public function getLastConnection(): ?\DateTime
{
return $this->subscriptions;
return $this->lastConnection;
}
public function addSubscription(Subscriptions $subscription): static
public function setLastConnection(?\DateTime $lastConnection): static
{
if (!$this->subscriptions->contains($subscription)) {
$this->subscriptions->add($subscription);
$subscription->setUsers($this);
}
$this->lastConnection = $lastConnection;
return $this;
}
public function removeSubscription(Subscriptions $subscription): static
public function getPhoneNumber(): ?string
{
if ($this->subscriptions->removeElement($subscription)) {
// set the owning side to null (unless already changed)
if ($subscription->getUsers() === $this) {
$subscription->setUsers(null);
}
}
return $this->phoneNumber;
}
public function setPhoneNumber(?string $phoneNumber): static
{
$this->phoneNumber = $phoneNumber;
return $this;
}
}

View File

@ -21,21 +21,24 @@ class UsersOrganizations
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?organizations $organization = null;
private ?Organizations $organization = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?roles $role = null;
private ?Roles $role = null;
#[ORM\Column(options: ['default' => true])]
private ?bool $isActive = null;
/**
* @var Collection<int, apps>
* @var Collection<int, Apps>
*/
#[ORM\ManyToMany(targetEntity: apps::class)]
#[ORM\ManyToMany(targetEntity: Apps::class)]
private Collection $apps;
#[ORM\Column(nullable:true, options: ['default' => 'CURRENT_TIMESTAMP'])]
private ?\DateTimeImmutable $createdAt = null;
public function __construct()
{
$this->apps = new ArrayCollection();
@ -117,4 +120,16 @@ class UsersOrganizations
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(?\DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\EventSubscriber;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
class LoginSubscriber implements EventSubscriberInterface
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public static function getSubscribedEvents()
{
return [
LoginSuccessEvent::class => 'onLoginSuccess',
];
}
public function onLoginSuccess(LoginSuccessEvent $event): void
{
$user = $event->getUser();
if($user) {
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $user->getUserIdentifier()]);
$user->setLastConnection(new \DateTime('now', new \DateTimeZone('Europe/Paris')));
$this->entityManager->persist($user);
$this->entityManager->flush();
}
}
}

View File

@ -2,7 +2,6 @@
namespace App\EventSubscriber;
use App\Service\ClientService;
use Doctrine\ORM\EntityManagerInterface;
use League\Bundle\OAuth2ServerBundle\Event\ScopeResolveEvent;
use League\Bundle\OAuth2ServerBundle\Repository\ScopeRepository;
@ -16,15 +15,14 @@ final class ScopeResolveListener implements EventSubscriberInterface
{
private ClientRepositoryInterface $clientRepository;
private LoggerInterface $logger;
private ClientService $clientService;
private EntityManagerInterface $entityManager;
public function __construct(ClientRepositoryInterface $clientRepository, LoggerInterface $logger, ClientService $clientService, EntityManagerInterface $entityManager)
public function __construct(ClientRepositoryInterface $clientRepository, LoggerInterface $logger, EntityManagerInterface $entityManager)
{
$this->logger = $logger;
// Inject the client repository
$this->clientRepository = $clientRepository;
$this->clientService = $clientService;
$this->entityManager = $entityManager;
}

29
src/Form/UserForm.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('email', EmailType::class, ['required' => true, 'label' => 'Email*'])
->add('name', TextType::class, ['required' => true, 'label' => 'Prénom*'])
->add('surname', TextType::class, ['required' => true, 'label' => 'Nom*'])
->add('phoneNumber', TextType::class, ['required' => false, 'label' => 'Numéro de téléphone']);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

View File

@ -33,28 +33,13 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
$this->getEntityManager()->flush();
}
// /**
// * @return User[] Returns an array of User objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('u.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?User
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
public function getAllActiveUsers(): array{
$queryBuilder = $this->createQueryBuilder('u')
->select('u.surname', 'u.email', 'u.id', 'u.isActive', 'u.name', 'u.pictureUrl')
// Remove this line: ->from(User::class, 'u')
->where('u.isActive = :isActive') // Also fixed the concatenation
->orderBy('u.surname', 'ASC');
$queryBuilder->setParameter('isActive', true);
return $queryBuilder->getQuery()->getResult();
}
}

View File

@ -6,9 +6,7 @@ use App\Entity\UsersOrganizations;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<UsersOrganizations>
*/
class UsersOrganizationsRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
@ -16,28 +14,18 @@ class UsersOrganizationsRepository extends ServiceEntityRepository
parent::__construct($registry, UsersOrganizations::class);
}
// /**
// * @return UsersOrganizations[] Returns an array of UsersOrganizations objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('u.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
public function findAllDistinctOrganizationsByUserId(int $userId): array
{
return $this->createQueryBuilder('uo')
->select('DISTINCT uo')
->leftJoin('uo.organization', 'o')
->leftJoin('uo.role', 'r')
->addSelect('o', 'r')
->where('uo.users = :userId', 'uo.isActive = :isActive')
->setParameter('userId', $userId)
->setParameter('isActive', true)
->getQuery()
->getResult();
}
// public function findOneBySomeField($value): ?UsersOrganizations
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@ -0,0 +1,103 @@
<?php
namespace App\Service;
use App\Entity\Roles;
use App\Entity\User;
use App\Entity\UsersOrganizations;
use Doctrine\ORM\EntityManagerInterface;
readonly class UserOrganizationService
{
public function __construct(private readonly EntityManagerInterface $entityManager) {}
/**
* Returns all organizations the given user belongs to,
* including the unique roles and apps the user has in each organization.
*
* @param User $user The user whose organizations are being fetched
* @return array<array{organization:object, roles:Roles[], apps:object[]}>
*/
public function getUserOrganizations(User $user): array
{
$userOrganizations = $this->entityManager
->getRepository(UsersOrganizations::class)
->findAllDistinctOrganizationsByUserId($user->getId());
$organizations = [];
foreach ($userOrganizations as $uo) {
$orgId = $uo->getOrganization()->getId();
// Initialize the organization entry if it doesn't exist
$organizations[$orgId] = $organizations[$orgId] ?? $this->createEmptyOrganizationBucket($uo);
// Aggregate roles & apps
$this->addRole($organizations[$orgId]['roles'], $uo->getRole());
$this->addApps($organizations[$orgId]['apps'], $uo->getApps());
}
$this->normalizeAppsIndexes($organizations);
return array_values($organizations);
}
/**
* Build the initial array structure for a fresh organization entry.
*
* @param UsersOrganizations $link
* @return array{organization:object, roles:Roles[], apps:array<int,object>}
*/
private function createEmptyOrganizationBucket(UsersOrganizations $link): array
{
return [
'organization' => $link->getOrganization(),
'roles' => [],
'apps' => [],
];
}
/**
* Add a Role entity to the roles array only if it is not already present (by ID).
*
* @param Roles[] &$roles
* @param Roles|null $role
*/
private function addRole(array &$roles, ?Roles $role): void
{
if ($role === null) {
return;
}
foreach ($roles as $existingRole) {
if ($existingRole->getId() === $role->getId()) {
return; // Already present
}
}
$roles[] = $role;
}
/**
* Merge one or many apps into the apps map, keeping only one entry per id.
*
* @param array<int,object> &$apps
* @param iterable $appsToAdd Collection returned by $userOrganizations->getApps()
*/
private function addApps(array &$apps, iterable $appsToAdd): void
{
foreach ($appsToAdd as $app) {
$apps[$app->getId()] = $apps[$app->getId()] ?? $app;
}
}
/**
* Convert apps from associative maps (keyed by id) to plain indexed arrays,
* so the final output is clean JSON-able.
*
* @param array &$organizations
*/
private function normalizeAppsIndexes(array &$organizations): void
{
foreach ($organizations as &$org) {
$org['apps'] = array_values($org['apps']);
}
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Service;
class UserService
{
public function __construct()
{
// Constructor logic if needed
}
/**
* Generate a random password for a new user until they set their own.
*/
public function generateRandomPassword(): string{
$length = 50; // Length of the password
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+';
$charactersLength = strlen($characters);
$randomPassword = '';
for ($i = 0; $i < $length; $i++) {
$randomPassword .= $characters[rand(0, $charactersLength - 1)];
}
return $randomPassword;
}
}

View File

@ -0,0 +1,11 @@
{% extends 'base.html.twig' %}
{% block title %}Page Not Found (404){% endblock %}
{% block body %}
<div class="d-flex justify-content-center align-items-center vh-100">
<h1>Oops! La page n'existe pas (404)</h1>
<p>La page que vous cherchez n'existe pas.</p>
<a href="{{ path('app_index') }}"> Retour à l'accueil </a>
</div>
{% endblock %}

View File

@ -22,11 +22,14 @@
</ul>
</div>
</li>
{# if user is Super Admin#}
{% if is_granted('ROLE_SUDALYS_ADMIN') %}
<li class="nav-item">
<a class="nav-link" href="#">
<a class="nav-link" href="{{ path('user_index') }}">
<i class="icon-grid menu-icon">{{ ux_icon('bi:menu-up', {height: '15px', width: '15px'}) }}</i>
<span class="menu-title">Menu 2</span>
<span class="menu-title">Users</span>
</a>
</li>
{% endif %}
</ul>
</nav>

View File

@ -0,0 +1,16 @@
{% block body %}
<div class="card">
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
<h2>{{ user.surname|capitalize }} {{ user.name|capitalize }}</h2>
<a href="{{ path('user_edit', {'id': user.id}) }}" class="btn btn-primary">Modifier</a>
</div>
<div class="card-body">
<p> <b>Email: </b>{{ user.email }}</p>
<p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }} à {{ user.lastConnection|date('H:m:s') }} </p>
<p><b>Compte crée le: </b>{{ user.createdAt|date('d/m/Y') }}</p>
<p><b>Numéro de téléphone: </b>{{ user.phoneNumber ? user.phoneNumber : 'Non renseigné' }}</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,85 @@
{% block body %}
<div class="card col-4 mt-3 me-3 user-org-card" style="cursor:pointer;" data-bs-toggle="collapse"
data-bs-target="#org-details-{{ organization.id }}" aria-expanded="false"
aria-controls="org-details-{{ organization.id }}">
<div class="card-title shadow-sm p-3 d-flex ">
<h2 class=" pe-2">{{ organization.name|capitalize }}</h2>
<i class="pt-2" id="arrow-icon-{{ organization.id }}">
{{ ux_icon('fa6-regular:circle-down', {height: '25px', width: '25px'}) }}
</i>
</div>
{# Information principale sur l'utilisateur dans l'organisation#}
<div class="card-body">
{# Affichage du plus haut role #}
<p><b>Role:</b>
{% if roles|length > 0 %}
{% set firstRole = roles[0] %}
{% if firstRole.name == "ROLE ADMIN SUDALYS" or firstRole.name == "ROLE ADMIN" %}
<span class="badge bg-danger">{{ firstRole.name|capitalize }}</span>
{% elseif firstRole.name == "ROLE USER" %}
<span class="badge bg-primary">{{ firstRole.name|capitalize }}</span>
{% else %}
<span class="badge bg-success">{{ firstRole.name|capitalize }}</span>
{% endif %}
{% else %}
Aucun rôle
{% endif %}
</p>
{# Affichage des applications dont l'utilisateur à accès #}
<div class="d-flex">
{% if apps is not empty %}
{% for app in apps %}
<div class="col">
{{ app.name }}
<img src="{{ asset(app.logoUrl) }}" alt="Logo {{ app.name }}" class="img-fluid ms-2" style="height: 20px; width: 20px;">
</div>
{% endfor %}
{% else %}
Aucune application associée.
{% endif %}
</div>
</div>
{# Détails supplémentaires sur l'organisation #}
<div class="collapse card-body border-top" id="org-details-{{ organization.id }}">
{% if roles|length > 1 %}
<p><b>Autres rôles:</b>
{% for role in roles|slice(1) %}
{% if role.name == "ROLE ADMIN SUDALYS" or role.name == "ROLE ADMIN" %}
<span class="badge bg-danger">{{ role.name|capitalize }}</span>
{% elseif role.name == "ROLE USER" %}
<span class="badge bg-primary">{{ role.name|capitalize }}</span>
{% else %}
<span class="badge bg-success">{{ role.name|capitalize }}</span>
{% endif %}
{% if not loop.last %} - {% endif %}
{% endfor %}
</p>
{% endif %}
<p><b>Membre depuis:</b> {{ organization.createdAt|date('d/m/Y') }}</p>
</div>
</div>
{% endblock %}
{% block javascript %}
<script>
document.addEventListener('DOMContentLoaded', function() {
var collapseEl = document.getElementById('org-details-{{ organization.id }}');
var arrowEl = document.getElementById('arrow-icon-{{ organization.id }}');
if (collapseEl && arrowEl) {
collapseEl.addEventListener('show.bs.collapse', function () {
arrowEl.innerHTML = `{{ ux_icon('fa6-regular:circle-up', {height: '25px', width: '25px'})|e('js') }}`;
});
collapseEl.addEventListener('hide.bs.collapse', function () {
arrowEl.innerHTML = `{{ ux_icon('fa6-regular:circle-down', {height: '25px', width: '25px'})|e('js') }}`;
});
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html.twig' %}
{% block body %}
<div class="w-100 h-100 p-5 m-auto">
<h1>Modifier l'utilisateur</h1>
{{ form_start(form, {'action': path('user_edit', {'id': user.id}), 'method': 'PUT'}) }}
{{ form_widget(form) }}
<button type="submit" class="btn btn-primary">Enregistrer</button>
{{ form_end(form) }}
</div>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends 'base.html.twig' %}
{% block title %}User Profile{% endblock %}
{% block body %}
<div class="w-100 h-100 p-5 m-auto ">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Gestion Utilisateurs</h1>
<a href="{{ path('user_new') }}" class="btn btn-primary">Ajouter un utilisateur</a>
</div>
<table class="table align-middle shadow">
<thead class="table-light shadow-sm">
<tr>
<th>Picture</th>
<th>Surname</th>
<th>Name</th>
<th>Email</th>
<th>Visualiser</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td></td>
<td>{{ user.surname }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>
<a href="{{ path('user_show', {'id': user.id}) }}" class="p-3 align-middle">
<i class="icon-grid menu-icon color-primary">
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
</a>
</td>
</tr>
{% endfor %}
{% if users|length == 0 %}
<tr>
<td colspan="5" class="text-center">Aucun utilisateur trouvé.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends 'base.html.twig' %}
{% block title %}Ajouter un utilisateur{% endblock %}
{% block body %}
<div class="w-100 h-100 p-5 m-auto">
<h1>Ajouter un utilisateur</h1>
<form method="post" action="{{ path('user_new') }}" enctype="multipart/form-data">
{{ form_start(form) }}
{{ form_widget(form) }}
<button type="submit" class="btn btn-primary">Enregistrer</button>
{{ form_end(form) }}
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends 'base.html.twig' %}
{% block body %}
<div class="col-md-10 m-auto p-5">
<div class="col d-flex justify-content-between align-items-center ">
<h1 class="mb-4">Gestion Utilisateur</h1>
<a href="{{ path('user_deactivate', {'id': user.id}) }}" class="btn btn-danger">Désactiver</a>
</div>
{% include 'elements/userInformation.html.twig' %}
<h1 class="mt-5 mb-4">Organisations</h1>
<div class="row">
{% if userOrganizations is empty %}
<div class="col-md-10 m-auto p-auto">
<h3>Aucune organisation associée à cet utilisateur.</h3>
</div>
{% else %}
{% for organization in userOrganizations %}
{% include 'elements/userOrganizationInformation.html.twig'
with {'organization': organization.organization, 'roles': organization.roles, 'apps': organization.apps} %}
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
{% block title %}
{% endblock %}