<?php
namespace App\Controller\Admin;
use App\Entity\BlackList;
use App\Entity\ControllerUser;
use App\Entity\DictionaryImport;
use App\Entity\Eg\Admin\Role;
use App\Entity\Eg\Admin\User;
use App\Entity\Eg\Admin\UserGroup;
use App\Entity\Eg\Osm\City;
use App\Entity\Eg\Osm\DataAggregator;
use App\Entity\Eg\Osm\Device;
use App\Entity\Eg\Osm\Organization;
use App\Entity\Eg\Osm\Parking;
use App\Entity\Eg\Osm\ParkingType;
use App\Entity\Eg\Osm\Zone;
use App\Entity\ElectricList;
use App\Entity\Fine;
use App\Entity\FineVerificationRule;
use App\Entity\LapPair;
use App\Entity\RequestLog;
use App\Entity\Route as RouteEntity;
use App\Entity\RouteLap;
use App\Entity\VerificationStatus;
use App\Menu\FineTabs;
use App\Module\AppModule;
use App\Module\Attribute\RequiresModule;
use App\Security\Scope\FineScopeResolver;
use App\Security\Scope\RequestLogScopeResolver;
use App\Service\ModuleAccessService;
use Doctrine\ORM\EntityManagerInterface;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Config\UserMenu;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class AdminDashbordController extends AbstractDashboardController
{
protected ?Request $request;
public function __construct(
protected Security $security,
RequestStack $requestStack,
protected EntityManagerInterface $manager,
protected ModuleAccessService $moduleAccess,
protected FineScopeResolver $fineScopeResolver,
protected RequestLogScopeResolver $requestLogScopeResolver,
protected TranslatorInterface $translator
) {
$this->request = $requestStack->getCurrentRequest();
}
public function configureAssets(): Assets
{
return Assets::new()
->addWebpackEncoreEntry('app');
}
public function configureActions(): Actions
{
return Actions::new()
->add(Crud::PAGE_INDEX, Action::EDIT)
->add(Crud::PAGE_INDEX, Action::DETAIL)
->add(Crud::PAGE_DETAIL, Action::INDEX)
->add(Crud::PAGE_EDIT, Action::INDEX)
->add(Crud::PAGE_NEW, Action::INDEX)
->update(Crud::PAGE_INDEX, Action::DETAIL, fn (Action $action): Action => $action->setLabel('Pokaż')->setIcon('far fa-eye'))
->update(Crud::PAGE_INDEX, Action::EDIT, fn (Action $action): Action => $action->setLabel('Edytuj')->setIcon('fas fa-pencil-alt'))
->update(Crud::PAGE_DETAIL, Action::INDEX, fn (Action $action): Action => $action->setLabel('Wróć do listy')->setIcon('fas fa-list'))
->update(Crud::PAGE_EDIT, Action::INDEX, fn (Action $action): Action => $action->setLabel('Wróć do listy')->setIcon('fas fa-list'))
->update(Crud::PAGE_NEW, Action::INDEX, fn (Action $action): Action => $action->setLabel('Wróć do listy')->setIcon('fas fa-list'))
->add(Crud::PAGE_EDIT, Action::SAVE_AND_CONTINUE);
}
public function configureCrud(): Crud
{
$crud = Crud::new()
->setDefaultSort(['id' => 'DESC'])
->setDateTimeFormat('y-MM-dd HH:mm')
->overrideTemplate('crud/index', 'index.html.twig')
->overrideTemplate('crud/detail', 'detail.html.twig')
->overrideTemplate('crud/new', 'new.html.twig')
->overrideTemplate('crud/edit', 'edit.html.twig')
->overrideTemplate('crud/field/association', 'fields/association_edit.html.twig')
->overrideTemplate('layout', 'layout.html.twig')
->setPageTitle('detail', '%entity_label_singular% #%entity_id%')
->setFormThemes(['form.html.twig', '@EasyAdmin/crud/form_theme.html.twig'])
->setSearchFields(null)
->showEntityActionsInlined();
if ($this->request->query->has('indexPageTitle')) {
$crud->setPageTitle(Crud::PAGE_INDEX, $this->request->query->get('indexPageTitle'));
}
return $crud;
}
/**
* Funkcja odpowiadająca za elementy znajdujące się w wyświetlanym menu
*/
public function configureMenuItems(): iterable
{
$menuItems = [MenuItem::linkToDashboard('Pulpit', 'fa fa-home')];
if ($this->moduleAccess->isEnabled(AppModule::DATA)) {
$dataItems = [];
$requestLogLinks = [];
if ($this->requestLogScopeResolver->canSeeAllTab()) {
$requestLogLinks[] = MenuItem::linkToCrud('Wszystkie', null, RequestLog::class);
}
if ($this->requestLogScopeResolver->can(RequestLogScopeResolver::VERB_VIEW, RequestLogScopeResolver::SCOPE_UNVERIFIED)) {
$requestLogLinks[] = MenuItem::linkToCrud('Niezweryfikowane', null, RequestLog::class)
->setQueryParameter('isVerified', false)
->setQueryParameter('indexPageTitle', RequestLogCrudController::INDEX_PAGE_TITLES['unverified']);
}
if ($this->requestLogScopeResolver->can(RequestLogScopeResolver::VERB_VIEW, RequestLogScopeResolver::SCOPE_VERIFIED)) {
$requestLogLinks[] = MenuItem::linkToCrud('Zweryfikowane', null, RequestLog::class)
->setQueryParameter('isVerified', true)
->setQueryParameter('indexPageTitle', RequestLogCrudController::INDEX_PAGE_TITLES['verified']);
}
if (\count($requestLogLinks) > 0) {
$dataItems[] = MenuItem::subMenu('Pojedyncze odczyty', 'fas fa-camera')->setSubItems($requestLogLinks);
}
if ($this->isGranted(Role::ROLE_LAPS_VIEW)) {
$dataItems[] = MenuItem::linkToCrud('Przejazdy', 'fas fa-map-marker-alt', RouteLap::class);
}
if ($this->isGranted(Role::ROLE_LAPS_PAIRS_VIEW)) {
$dataItems[] = MenuItem::linkToCrud('Pary przejazdów', 'fas fa-route', LapPair::class);
}
// widoczność zakładek zawiadomień pochodzą w całości z FineTabs
$fineLinks = [];
foreach (FineTabs::visibleFor($this->fineScopeResolver) as $tab) {
$link = MenuItem::linkToCrud($tab['label'], null, Fine::class);
if ($tab['status'] !== null) {
$link
->setQueryParameter('verificationStatus', $tab['status'])
->setQueryParameter('indexPageTitle', $tab['title']);
}
$fineLinks[] = $link;
}
if (\count($fineLinks) > 0) {
$dataItems[] = MenuItem::subMenu('Zawiadomienia', 'fas fa-comment-dollar')->setSubItems($fineLinks);
}
// nagłówek sekcji tylko wtedy, gdy cokolwiek pod nim jest
if (\count($dataItems) > 0) {
$menuItems[] = MenuItem::section('Dane');
$menuItems = \array_merge($menuItems, $dataItems);
}
}
if ($this->moduleAccess->isEnabled(AppModule::PARKING)) {
$parkingItems = [
[Role::ROLE_ORG_VIEW, 'Organizacje', 'fa fa-building', Organization::class],
[Role::ROLE_CITY_VIEW, 'Miasta', 'fa fa-city', City::class],
[Role::ROLE_ZONES_VIEW, 'Strefy', 'fa fa-draw-polygon', Zone::class],
[Role::ROLE_AGG_VIEW, 'Agregatory', 'fa fa-road', DataAggregator::class],
[Role::ROLE_PARK_TYPE_VIEW, 'Typy parkingów', 'fa fa-wheelchair', ParkingType::class],
[Role::ROLE_PARKING_VIEW, 'Parkingi', 'fa fa-parking', Parking::class],
];
$visibleParkingItems = [];
foreach ($parkingItems as [$role, $label, $icon, $entityFqcn]) {
if ($this->isGranted($role)) {
$visibleParkingItems[] = MenuItem::linkToCrud($label, $icon, $entityFqcn);
}
}
if (\count($visibleParkingItems) > 0) {
$menuItems[] = MenuItem::section('Strefy i Parkingi');
$menuItems = \array_merge($menuItems, $visibleParkingItems);
}
}
if ($this->moduleAccess->isEnabled(AppModule::ROUTES) && $this->isGranted(Role::ROLE_ROUTES_VIEW)) {
$menuItems[] = MenuItem::section('Trasy');
$menuItems[] = MenuItem::linkToCrud('Zarządzanie trasami', 'fas fa-road', RouteEntity::class);
}
if ($this->moduleAccess->isEnabled(AppModule::MAP) && $this->isGranted(Role::ROLE_MAP_VIEW)) {
$menuItems[] = MenuItem::section('');
$menuItems[] = MenuItem::linkToRoute('Mapa', 'fa fa-map', 'map');
}
if ($this->moduleAccess->isEnabled(AppModule::STATISTICS)) {
$statsItems = [];
if ($this->isGranted(Role::ROLE_STATS_VIEW)) {
$statsItems[] = MenuItem::linktoRoute('Statystyki', 'fa fa-chart-bar', 'statistics');
}
if ($this->isGranted(Role::ROLE_USER_STATS_VIEW)) {
$statsItems[] = MenuItem::linktoRoute('Zestawienia zawiadomień', 'fa fa-chart-bar', 'fine_statements');
$statsItems[] = MenuItem::linktoRoute('Zestawienia odczytów', 'fa fa-chart-bar', 'request_log_statements');
}
if ($this->isGranted(Role::ROLE_STATS_VIEW)) {
$statsItems[] = MenuItem::linktoRoute('Zestawienia eksportów', 'fa fa-chart-bar', 'log_exports_statements');
}
if (\count($statsItems) > 0) {
$menuItems[] = MenuItem::section('Statystyki i zestawienia');
$menuItems = [...$menuItems, ...$statsItems];
}
}
if ($this->moduleAccess->isEnabled(AppModule::CONFIG)) {
$canSeeSettings = $this->isGranted(Role::ROLE_CONFIG_VIEW);
$canSeeJsonConfig = $this->isGranted(Role::ROLE_CONFIG_CREATOR);
if ($canSeeSettings || $canSeeJsonConfig) {
$menuItems[] = MenuItem::section('Konfiguracja');
}
if ($canSeeSettings) {
$menuItems[] = MenuItem::linkToRoute('Ustawienia', 'fas fa-sliders-h', 'config_settings_index');
}
if ($canSeeJsonConfig) {
$menuItems[] = MenuItem::linkToRoute(
$this->translator->trans('config.json.menu_label', [], 'config'),
'fas fa-file-code',
'config_json_index',
);
}
}
// nagłówek sekcji "Administracja" tylko raz i tylko gdy pod nim faktycznie pojawi się jakaś pozycja
// brak pustej sekcji, gdy użytkownik nie ma żadnej z ról administracyjnych
$adminSectionShown = false;
$showAdminSection = function () use (&$menuItems, &$adminSectionShown): void {
if (!$adminSectionShown) {
$menuItems[] = MenuItem::section('Administracja');
$adminSectionShown = true;
}
};
if ($this->moduleAccess->isEnabled(AppModule::ADMIN)) {
if ($this->isGranted(Role::ROLE_USERS_VIEW)) {
$showAdminSection();
$menuItems[] = MenuItem::linkToCrud('Użytkownicy', 'fas fa-user', User::class);
}
if ($this->isGranted(Role::ROLE_CONTROLLERS_VIEW)) {
$showAdminSection();
$menuItems[] = MenuItem::linkToCrud('Kontrolerzy', 'fas fa-mobile-alt', ControllerUser::class);
}
if ($this->isGranted(Role::ROLE_R_GROUPS_VIEW)) {
$showAdminSection();
$menuItems[] = MenuItem::linkToCrud('Grupy Uprawnień', 'fas fa-users-cog', UserGroup::class);
}
if ($this->isGranted(Role::ROLE_IMPORT_VIEW)) {
$showAdminSection();
$menuItems[] = MenuItem::linkToCrud('Import danych', 'fas fa-file-import', DictionaryImport::class);
}
if ($this->isGranted(Role::ROLE_CONTROL_BOX_VIEW)) {
$showAdminSection();
$menuItems[] = MenuItem::linkToCrud('Zestawy kontrolerskie', 'fa fa-video-camera', Device::class);
}
}
if ($this->moduleAccess->isEnabled(AppModule::ELECTRIC) && $this->isGranted(Role::ROLE_ELECTRIC_VIEW)) {
$showAdminSection();
$menuItems[] = MenuItem::linkToCrud('Lista samochodów elektrycznych', 'fas fa-charging-station', ElectricList::class);
}
if ($this->moduleAccess->isEnabled(AppModule::BLACKLIST) && $this->isGranted(Role::ROLE_BLACK_LIST_VIEW)) {
$showAdminSection();
$menuItems[] = MenuItem::linkToCrud('Czarna lista rejestracji', 'fas fa-ban', BlackList::class);
}
// reguły klasyfikacji zawiadomień - moduł danych, bo bez zawiadomień nie mają czego klasyfikować
if ($this->moduleAccess->isEnabled(AppModule::DATA) && $this->isGranted(Role::ROLE_FINE_VERIFICATION_RULES_VIEW)) {
$showAdminSection();
$menuItems[] = MenuItem::linkToCrud(
$this->translator->trans('fine_verification_rule.menu_label', [], 'fine_verification_rule'),
'fas fa-code-branch',
FineVerificationRule::class
);
}
return $menuItems;
}
public function configureUserMenu(UserInterface $user): UserMenu
{
$url = $this->container->get(AdminUrlGenerator::class)
->setAction('changePassword')
->setController(UserCrudController::class)
->setEntityId($user->getId())
->set('referrer', urlencode($this->request->getRequestUri()))
->generateUrl();
return parent::configureUserMenu($user)
->addMenuItems([
MenuItem::linkToUrl('Zmień hasło', null, $url)
]);
}
/**
* Metoda do konfiguracji widoku pulpitu
*/
public function configureDashboard(): Dashboard
{
return Dashboard::new()
->setTitle('Admin kamery')
->setFaviconPath('image/logo/favicon.png')
->disableDarkMode();
}
#[Route(path: '/admin', name: 'admin')]
public function index(): Response
{
$requestLogIndexes = $this->determineMenuIndex('Pojedyncze odczyty');
$routeLapIndexes = $this->determineMenuIndex('Przejazdy');
$lapPairIndexes = $this->determineMenuIndex('Pary przejazdów');
$fineTab = $this->defaultFineTab();
$fineIndexes = $this->determineMenuIndex('Zawiadomienia', $fineTab['label'] ?? null);
$requestLogUrl = $this->container->get(AdminUrlGenerator::class)
->setAction('index')
->setController(RequestLogCrudController::class)
->generateUrl();
$routeLapUrl = $this->container->get(AdminUrlGenerator::class)
->setAction('index')
->setController(RouteLapCrudController::class)
->generateUrl();
$lapPairUrl = $this->container->get(AdminUrlGenerator::class)
->setAction('index')
->setController(LapPairCrudController::class)
->generateUrl();
$fineUrlBuilder = $this->container->get(AdminUrlGenerator::class)
->setAction('index')
->setController(FineCrudController::class)
->set('menuIndex', $fineIndexes['menu'])
->set('submenuIndex', $fineIndexes['submenu']);
// zakladka "Wszystkie" nie filtruje po statusie, wiec nie dokladamy jej parametrow
if ($fineTab !== null && $fineTab['status'] !== null) {
$fineUrlBuilder
->set('verificationStatus', $fineTab['status'])
->set('indexPageTitle', $fineTab['title']);
}
$fineUrl = $fineUrlBuilder->generateUrl();
$mapUrl = $this->redirectToRoute('map')->getTargetUrl();
return $this->render('start.html.twig', [
'requestLogUrl' => $requestLogUrl,
'routeLapUrl' => $routeLapUrl,
'lapPairUrl' => $lapPairUrl,
'fineUrl' => $fineUrl,
'mapUrl' => $mapUrl,
'serverData' => json_encode($_SERVER)
]);
}
#[Route(path: '/', name: 'blank')]
public function blank(): Response
{
return $this->redirectToRoute('admin');
}
#[Route(path: '/admin/map', name: 'map')]
#[RequiresModule(AppModule::MAP)]
public function showMap(): Response
{
$this->denyAccessUnlessGranted(Role::ROLE_MAP_VIEW);
// URL do edycji pojedynczych odczytów
// Aby podmienić wystarczy złapać parametr 'entityId=0' i podmienić go na odpowiednią wartość
$url = $this->container->get(AdminUrlGenerator::class)
->setAction(Crud::PAGE_EDIT)
->setController(RequestLogCrudController::class)
->setEntityId(0)
->set('referrer', '/admin/map')
->generateUrl();
return $this->render('map.html.twig', [
'requestLogURL' => $url
]);
}
#[Route(path: '/admin/statistics', name: 'statistics')]
#[RequiresModule(AppModule::STATISTICS)]
public function renderStatistics(): Response
{
$this->denyAccessUnlessGranted(Role::ROLE_STATS_VIEW);
return $this->render('stats/statistics.html.twig');
}
#[Route(path: '/admin/fine-statements', name: 'fine_statements')]
#[RequiresModule(AppModule::STATISTICS)]
public function renderFineStatements(): Response
{
$this->denyAccessUnlessGranted(Role::ROLE_USER_STATS_VIEW);
return $this->render('stats/fine_statements.html.twig', [
'selectableUsers' => $this->manager->getRepository(User::class)->getAllUsernames(true)
]);
}
#[Route(path: '/admin/request-log-statements', name: 'request_log_statements')]
#[RequiresModule(AppModule::STATISTICS)]
public function renderRequestLogStatements(): Response
{
$this->denyAccessUnlessGranted(Role::ROLE_USER_STATS_VIEW);
return $this->render('stats/request_log_statements.html.twig', [
'selectableUsers' => $this->manager->getRepository(User::class)->getAllUsernames(true)
]);
}
#[Route(path: '/admin/log-exports-statements', name: 'log_exports_statements')]
#[RequiresModule(AppModule::STATISTICS)]
public function renderLogExportsStatements(): Response
{
$this->denyAccessUnlessGranted(Role::ROLE_STATS_VIEW);
return $this->render('stats/log_exports_statements.html.twig');
}
/**
* Zakładka zawiadomień, na którą prowadzi kafelek "Zawiadomienia" na pulpicie
*
* "Do weryfikacji" zostaje domyślna, ale rola o wąskim zakresie jej nie widzi - wtedy kafelek
* prowadzi na pierwszą zakładkę dostępną dla konta, zamiast na komunikat o braku uprawnień
*
* @return array{status: int|null, label: string, title: string}|null
*/
private function defaultFineTab(): ?array
{
$tabs = FineTabs::visibleFor($this->fineScopeResolver);
foreach ($tabs as $tab) {
if ($tab['status'] === VerificationStatus::TO_VERIFY) {
return $tab;
}
}
return $tabs[0] ?? null;
}
/**
* Metoda służąca do obliczania indexu danej encji w menu
*/
private function determineMenuIndex(string $entityLabel, ?string $subEntityLabel = null)
{
$indexes = [
'menu' => -1,
'submenu' => -1
];
foreach ($this->configureMenuItems() as $index => $item) {
if ($item->getAsDto()->getLabel() == $entityLabel) {
$indexes['menu'] = $index;
if ($subEntityLabel) {
foreach ($item->getAsDto()->getSubItems() as $subIndex => $subItem) {
if ($subItem->getLabel() == $subEntityLabel) {
$indexes['submenu'] = $subIndex;
}
}
}
}
}
return $indexes;
}
}