add grid like view to the available apps

This commit is contained in:
Charles 2026-02-02 15:51:49 +01:00
parent 8aa1dfbfff
commit e0d2457e26
4 changed files with 153 additions and 22 deletions

View File

@ -5,8 +5,9 @@ export default class extends Controller {
static values = { static values = {
application: String, application: String,
organization: String, organization: String,
user: Number,
} }
static targets = ['hidden', 'submitBtn'] static targets = ['hidden', 'submitBtn', 'appList']
connect() { connect() {
// Map each editor to its toolbar and hidden field // Map each editor to its toolbar and hidden field
@ -40,6 +41,9 @@ export default class extends Controller {
hiddenTarget.value = quill.root.innerHTML hiddenTarget.value = quill.root.innerHTML
}) })
} }
if(this.userValue){
this.loadApplications();
}
} }
handleAuthorizeSubmit(event) { handleAuthorizeSubmit(event) {
@ -107,4 +111,59 @@ export default class extends Controller {
alert('Erreur lors de l\'action'); alert('Erreur lors de l\'action');
}); });
} }
async loadApplications() {
if (!this.userValue) return;
try {
// Note: Ensure the URL matches your route prefix (e.g. /application/user/123)
// Adjust the base path below if your controller route is prefixed!
const response = await fetch(`/application/user/${this.userValue}`);
if (!response.ok) throw new Error("Failed to load apps");
const apps = await response.json();
this.renderApps(apps);
} catch (error) {
console.error(error);
this.appListTarget.innerHTML = `<span class="text-danger small">Erreur</span>`;
}
}
renderApps(apps) {
if (apps.length === 0) {
// Span 2 columns if empty so the message is centered
this.appListTarget.innerHTML = `<span class="text-muted small" style="grid-column: span 2; text-align: center;">Aucune application</span>`;
return;
}
const html = apps.map(app => {
const url = `https://${app.subDomain}.solutions-easy.moi`;
// Check for logo string vs object
const logoSrc = (typeof app.logoMiniUrl === 'string') ? app.logoMiniUrl : '';
// Render Icon (Image or Fallback)
const iconHtml = logoSrc
? `<img src="${logoSrc}" style="width:32px; height:32px; object-fit:contain; margin-bottom: 5px;">`
: `<i class="bi bi-box-arrow-up-right text-primary" style="font-size: 24px; margin-bottom: 5px;"></i>`;
// Return a Card-like block
return `
<a href="${url}" target="_blank"
class="d-flex flex-column align-items-center justify-content-center p-3 rounded text-decoration-none text-dark bg-light-hover"
style="transition: background 0.2s; height: 100%;">
${iconHtml}
<span class="fw-bold text-center text-truncate w-100" style="font-size: 0.85rem;">
${app.name}
</span>
</a>
`;
}).join('');
this.appListTarget.innerHTML = html;
}
} }

View File

@ -4,26 +4,32 @@ namespace App\Controller;
use App\Entity\Apps; use App\Entity\Apps;
use App\Entity\Organizations; use App\Entity\Organizations;
use App\Repository\UserRepository;
use App\Service\ActionService; use App\Service\ActionService;
use App\Service\ApplicationService; use App\Service\ApplicationService;
use App\Service\LoggerService; use App\Service\LoggerService;
use App\Service\UserOrganizationAppService;
use App\Service\UserService; use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; use Symfony\Component\Asset\Packages;
#[Route(path: '/application', name: 'application_')] #[Route(path: '/application', name: 'application_')]
class ApplicationController extends AbstractController class ApplicationController extends AbstractController
{ {
public function __construct(private readonly EntityManagerInterface $entityManager, public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly UserService $userService, private readonly UserService $userService,
private readonly ActionService $actionService, private readonly ActionService $actionService,
private readonly LoggerService $loggerService, private readonly LoggerService $loggerService,
private readonly ApplicationService $applicationService) private readonly ApplicationService $applicationService,
private readonly UserRepository $userRepository,
private Packages $assetsManager,
private readonly UserOrganizationAppService $userOrganizationAppService)
{ {
} }
@ -172,4 +178,30 @@ class ApplicationController extends AbstractController
return new Response('', Response::HTTP_OK); return new Response('', Response::HTTP_OK);
} }
#[Route(path:'/user/{id}', name: 'user', methods: ['GET'])]
public function getApplicationUsers(int $id): JSONResponse
{
$user = $this->userRepository->find($id);
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
if (!$user) {
$this->loggerService->logEntityNotFound('User', ['message'=> 'User not found for application list'], $actingUser->getId());
return new JsonResponse(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
}
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$applications = $this->entityManager->getRepository(Apps::class)->findAll();
}else{
$applications = $this->userOrganizationAppService->getUserApplications($user);
}
$data = array_map(function($app) {
return [
'name' => $app->getName(),
'subDomain' => $app->getSubDomain(),
'logoMiniUrl' => $this->assetsManager->getUrl($app->getLogoMiniUrl()),
];
}, $applications);
return new JsonResponse($data, Response::HTTP_OK);
}
} }

View File

@ -248,4 +248,26 @@ class UserOrganizationAppService
$uoaAdmin->setIsActive(true); $uoaAdmin->setIsActive(true);
} }
} }
/**
* Get users applications links for a given user
*
* @param User $user
* @return Apps[]
*/
public function getUserApplications(User $user): array
{
$uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
$apps = [];
foreach ($uos as $uo) {
$uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $uo, 'isActive' => true]);
foreach ($uoas as $uoa) {
$app = $uoa->getApplication();
if (!in_array($app, $apps, true)) {
$apps[] = $app;
}
}
}
return $apps;
}
} }

View File

@ -72,6 +72,35 @@
</div> </div>
</div> </div>
</li> </li>
<li class="nav-item dropdown nav-notif"
data-controller="application"
data-application-user-value="{{ app.user.id }}"
>
<a id="applicationDropdown"
class="nav-link count-indicator dropdown-toggle m-auto"
href="#"
data-bs-toggle="dropdown"
aria-expanded="false"
data-action="click->application#loadApps">
<i class="mx-0">{{ ux_icon('bi:grid-3x3-gap', {height: '20px', width: '20px'}) }}</i>
</a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown preview-list"
aria-labelledby="applicationDropdown"
style="max-height: 400px; overflow-y: auto; min-width: 320px;">
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-bottom">
<p class="mb-0 font-weight-normal dropdown-header text-bold">Applications</p>
</div>
<div class="px-3 py-2">
<div data-application-target="appList"
style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<span class="text-muted small">Chargement...</span>
</div>
</div>
</div>
</li>
<li class="nav-item dropdown nav-profile"> <li class="nav-item dropdown nav-profile">
<a id="profileDropdown" class="nav-link count-indicator dropdown-toggle m-auto" href="#" data-bs-toggle="dropdown"> <a id="profileDropdown" class="nav-link count-indicator dropdown-toggle m-auto" href="#" data-bs-toggle="dropdown">
<div id="profil" class="rounded-circle bg-secondary d-flex"> <div id="profil" class="rounded-circle bg-secondary d-flex">
@ -85,25 +114,14 @@
{% endif %} {% endif %}
</div> </div>
</a> </a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown px-2" aria-labelledby="profileDropdown" data-bs-popper="static"> <div class="dropdown-menu dropdown-menu-right navbar-dropdown px-2"
aria-labelledby="profileDropdown"
data-bs-popper="static">
<a class="dropdown-item border-bottom" style="padding-left: 8px;" href="{{ path('user_show', {'id': app.user.id}) }}"> <a class="dropdown-item border-bottom" style="padding-left: 8px;" href="{{ path('user_show', {'id': app.user.id}) }}">
<i class="me-2">{{ ux_icon('bi:gear', {height: '20px', width: '20px'}) }}</i> <i class="me-2">{{ ux_icon('bi:gear', {height: '20px', width: '20px'}) }}</i>
Profil Profil
</a> </a>
<div style="padding:8px 0;" class="row border-bottom">
<div class="col-2 m-auto">
<i >{{ ux_icon('bi:menu-up', {height: '20px', width: '20px'}) }}</i>
</div>
<div class="col-9">
<a href="http://client.solutions-easy.moi"> Client </a>
{# <select class="form-control">
<option>Exploit</options>
<option>Monithor</options>
<option>Check</options>
<option>Access</options>
</select> #}
</div>
</div>
<a class="dropdown-item" style="padding-left: 8px;" href="{{ path('sso_logout') }}"> <a class="dropdown-item" style="padding-left: 8px;" href="{{ path('sso_logout') }}">
<i class="me-2">{{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }}</i> <i class="me-2">{{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }}</i>
Deconnexion Deconnexion