add admin panel
Co-authored-by: Jan-Marlon Leibl <jleibl@proton.me> Reviewed-on: #20
This commit is contained in:
parent
e3a67adf63
commit
2ccba65185
5
.env
5
.env
@ -17,3 +17,8 @@ DATABASE_URL="postgresql://${DB_USER:-db}:${DB_PW:-db}@${DB_HOST:-db}:${DB_PORT:
|
|||||||
STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY}
|
STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY}
|
||||||
STRIPE_SECRET_KEY=${STRIPE_PUBLIC_KEY}
|
STRIPE_SECRET_KEY=${STRIPE_PUBLIC_KEY}
|
||||||
###
|
###
|
||||||
|
|
||||||
|
### ADMIN PANEL
|
||||||
|
USER_PASSWORD=${USER_PASSWORD}
|
||||||
|
ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
||||||
|
###
|
4
.env.dev
4
.env.dev
@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
APP_SECRET=5a866a6ab3ce4ef99240ba643868b123
|
APP_SECRET=5a866a6ab3ce4ef99240ba643868b123
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
USER_PASSWORD=\$2y\$13\$z/XlUykvakLzDR8TeFrQk.jmGuOKOcULlMY/m17aWmkY4f4NrIaam
|
||||||
|
ADMIN_PASSWORD=\$2y\$13\$z/XlUykvakLzDR8TeFrQk.jmGuOKOcULlMY/m17aWmkY4f4NrIaam
|
@ -12,6 +12,7 @@
|
|||||||
"doctrine/doctrine-bundle": "^2.13",
|
"doctrine/doctrine-bundle": "^2.13",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.4",
|
"doctrine/doctrine-migrations-bundle": "^3.4",
|
||||||
"doctrine/orm": "^3.3",
|
"doctrine/orm": "^3.3",
|
||||||
|
"easycorp/easyadmin-bundle": "^4.24",
|
||||||
"php-flasher/flasher-noty-symfony": "^2.1",
|
"php-flasher/flasher-noty-symfony": "^2.1",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"stripe/stripe-php": "^16.4",
|
"stripe/stripe-php": "^16.4",
|
||||||
|
1050
composer.lock
generated
1050
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -15,4 +15,6 @@ return [
|
|||||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
||||||
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
|
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
|
||||||
Symfony\UX\Icons\UXIconsBundle::class => ['all' => true],
|
Symfony\UX\Icons\UXIconsBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
|
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
55
config/packages/security.yaml
Normal file
55
config/packages/security.yaml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
security:
|
||||||
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
|
password_hashers:
|
||||||
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
|
providers:
|
||||||
|
users_in_memory:
|
||||||
|
memory:
|
||||||
|
users:
|
||||||
|
user: { password: '%env(USER_PASSWORD)%', roles: ['ROLE_ADMIN'] }
|
||||||
|
admin: { password: '%env(ADMIN_PASSWORD)%', roles: ['ROLE_SUPER_ADMIN'] }
|
||||||
|
|
||||||
|
firewalls:
|
||||||
|
dev:
|
||||||
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
|
security: false
|
||||||
|
main:
|
||||||
|
lazy: true
|
||||||
|
provider: users_in_memory
|
||||||
|
custom_authenticator: App\Security\AdminPanelAuthenticator
|
||||||
|
form_login:
|
||||||
|
login_path: /admin/login
|
||||||
|
check_path: /admin/login
|
||||||
|
logout:
|
||||||
|
path: /admin/logout
|
||||||
|
target: /admin/login
|
||||||
|
# activate different ways to authenticate
|
||||||
|
# https://symfony.com/doc/current/security.html#the-firewall
|
||||||
|
|
||||||
|
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||||
|
# switch_user: true
|
||||||
|
|
||||||
|
# Easy way to control access for large sections of your site
|
||||||
|
# Note: Only the *first* access control that matches will be used
|
||||||
|
|
||||||
|
role_hierarchy:
|
||||||
|
ROLE_SUPER_ADMIN: ROLE_ADMIN
|
||||||
|
|
||||||
|
access_control:
|
||||||
|
- { path: ^/admin/login, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/admin, roles: ROLE_ADMIN }
|
||||||
|
# - { path: ^/profile, roles: ROLE_USER }
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
security:
|
||||||
|
password_hashers:
|
||||||
|
# By default, password hashers are resource intensive and take time. This is
|
||||||
|
# important to generate secure password hashes. In tests however, secure hashes
|
||||||
|
# are not important, waste resources and increase test times. The following
|
||||||
|
# reduces the work factor to the lowest possible values.
|
||||||
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||||
|
algorithm: auto
|
||||||
|
cost: 4 # Lowest possible value for bcrypt
|
||||||
|
time_cost: 3 # Lowest possible value for argon
|
||||||
|
memory_cost: 10 # Lowest possible value for argon
|
7
config/packages/translation.yaml
Normal file
7
config/packages/translation.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
framework:
|
||||||
|
default_locale: de
|
||||||
|
translator:
|
||||||
|
default_path: '%kernel.project_dir%/translations'
|
||||||
|
fallbacks:
|
||||||
|
- de
|
||||||
|
providers:
|
3
config/routes/security.yaml
Normal file
3
config/routes/security.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
_security_logout:
|
||||||
|
resource: security.route_loader.logout
|
||||||
|
type: service
|
65
src/Controller/Admin/CustomerCrudController.php
Normal file
65
src/Controller/Admin/CustomerCrudController.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\Payment;
|
||||||
|
use App\Enum\TicketData;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\CollectionField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\TelephoneField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||||
|
use App\Form\TicketType;
|
||||||
|
|
||||||
|
class CustomerCrudController extends AbstractCrudController
|
||||||
|
{
|
||||||
|
public static function getEntityFqcn(): string
|
||||||
|
{
|
||||||
|
return Customer::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureFields(string $pageName): iterable
|
||||||
|
{
|
||||||
|
yield TextField::new('firstname', 'Vorname');
|
||||||
|
yield TextField::new('lastname', 'Nachname');
|
||||||
|
yield EmailField::new('email', 'E-Mail');
|
||||||
|
yield TelephoneField::new('phone', 'Telefon');
|
||||||
|
yield AssociationField::new('payment', 'Total')
|
||||||
|
->setCrudController(PaymentCrudController::class)
|
||||||
|
->formatValue(fn(?Payment $payment) => ($payment?->getTotal() ?? 0.0) . ' €')
|
||||||
|
->hideOnIndex()
|
||||||
|
->hideOnForm();
|
||||||
|
yield CollectionField::new('tickets')
|
||||||
|
->allowAdd()
|
||||||
|
->allowDelete()
|
||||||
|
->setEntryType(TicketType::class)
|
||||||
|
->setFormTypeOptions(['by_reference' => false])
|
||||||
|
->setTemplatePath('admin/customer_tickets.html.twig')
|
||||||
|
->hideOnIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function configureActions(Actions $actions): Actions
|
||||||
|
{
|
||||||
|
return $actions
|
||||||
|
->add(Crud::PAGE_INDEX, Action::DETAIL)
|
||||||
|
->disable(Action::DELETE)
|
||||||
|
->setPermission(Action::NEW, 'ROLE_SUPER_ADMIN')
|
||||||
|
->setPermission(Action::EDIT, 'ROLE_SUPER_ADMIN');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureCrud(Crud $crud): Crud
|
||||||
|
{
|
||||||
|
return $crud
|
||||||
|
->setPageTitle(Crud::PAGE_INDEX, 'Kunden')
|
||||||
|
->setPageTitle(Crud::PAGE_DETAIL, 'Kunden')
|
||||||
|
->showEntityActionsInlined();
|
||||||
|
}
|
||||||
|
}
|
44
src/Controller/Admin/PaymentCrudController.php
Normal file
44
src/Controller/Admin/PaymentCrudController.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Entity\Payment;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
|
||||||
|
|
||||||
|
class PaymentCrudController extends AbstractCrudController
|
||||||
|
{
|
||||||
|
public static function getEntityFqcn(): string
|
||||||
|
{
|
||||||
|
return Payment::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureFields(string $pageName): iterable
|
||||||
|
{
|
||||||
|
yield AssociationField::new('customer', 'Kunde')
|
||||||
|
->setCrudController(CustomerCrudController::class)
|
||||||
|
->formatValue(fn($value, $payment) => $payment->getCustomer()->getEmail());
|
||||||
|
yield BooleanField::new('completed', 'Bezahlt')->renderAsSwitch(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureActions(Actions $actions): Actions
|
||||||
|
{
|
||||||
|
return $actions
|
||||||
|
->add(Crud::PAGE_INDEX, Action::DETAIL)
|
||||||
|
->disable(Action::DELETE)
|
||||||
|
->disable(Action::NEW)
|
||||||
|
->setPermission(Action::EDIT, 'ROLE_SUPER_ADMIN');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureCrud(Crud $crud): Crud
|
||||||
|
{
|
||||||
|
return $crud
|
||||||
|
->setPageTitle(Crud::PAGE_INDEX, 'Zahlungen')
|
||||||
|
->setPageTitle(Crud::PAGE_DETAIL, 'Zahlung')
|
||||||
|
->showEntityActionsInlined();
|
||||||
|
}
|
||||||
|
}
|
22
src/Controller/Admin/SecurityController.php
Normal file
22
src/Controller/Admin/SecurityController.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
|
|
||||||
|
class SecurityController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route(path: '/admin/login', name: 'admin_login', methods: [Request::METHOD_GET, Request::METHOD_POST])]
|
||||||
|
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||||
|
{
|
||||||
|
return $this->render('admin/login.html.twig', [
|
||||||
|
'last_username' => $authenticationUtils->getLastUsername(),
|
||||||
|
'error' => $authenticationUtils->getLastAuthenticationError(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
54
src/Controller/Admin/TicketCrudController.php
Normal file
54
src/Controller/Admin/TicketCrudController.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\Ticket;
|
||||||
|
use App\Enum\FoodData;
|
||||||
|
use App\Enum\TicketData;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
|
||||||
|
|
||||||
|
class TicketCrudController extends AbstractCrudController
|
||||||
|
{
|
||||||
|
public static function getEntityFqcn(): string
|
||||||
|
{
|
||||||
|
return Ticket::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureFields(string $pageName): iterable
|
||||||
|
{
|
||||||
|
yield ChoiceField::new('type', 'Name')
|
||||||
|
->setChoices(TicketData::TYPES);
|
||||||
|
|
||||||
|
yield ChoiceField::new('foodType', 'Ernährung')
|
||||||
|
->setChoices(FoodData::TYPES);
|
||||||
|
|
||||||
|
yield AssociationField::new('customer', 'Kunde')
|
||||||
|
->setCrudController(CustomerCrudController::class)
|
||||||
|
->formatValue(fn(Customer $customer) => $customer->getEmail())
|
||||||
|
->hideOnForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function configureActions(Actions $actions): Actions
|
||||||
|
{
|
||||||
|
return $actions
|
||||||
|
->add(Crud::PAGE_INDEX, Action::DETAIL)
|
||||||
|
->disable(Action::DELETE)
|
||||||
|
->disable(Action::NEW)
|
||||||
|
->setPermission(Action::EDIT, 'ROLE_SUPER_ADMIN');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureCrud(Crud $crud): Crud
|
||||||
|
{
|
||||||
|
return $crud
|
||||||
|
->setPageTitle(Crud::PAGE_INDEX, 'Tickets')
|
||||||
|
->setPageTitle(Crud::PAGE_DETAIL, 'Ticket')
|
||||||
|
->showEntityActionsInlined();
|
||||||
|
}
|
||||||
|
}
|
47
src/Controller/Admin/ViewController.php
Normal file
47
src/Controller/Admin/ViewController.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\Payment;
|
||||||
|
use App\Entity\Ticket;
|
||||||
|
use App\Repository\PaymentRepository;
|
||||||
|
use App\Repository\TicketRepository;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[AdminDashboard(routePath: '/admin', routeName: 'admin')]
|
||||||
|
class ViewController extends AbstractDashboardController
|
||||||
|
{
|
||||||
|
public function __construct(private readonly PaymentRepository $paymentRepository, private readonly TicketRepository $ticketRepository)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin', name: 'admin')]
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
return $this->render('admin/dashboard.html.twig', [
|
||||||
|
'totalMoney' => $this->paymentRepository->getTotalMoneyMade(),
|
||||||
|
'foodData' => $this->ticketRepository->getFoodData(),
|
||||||
|
'totalTickets' => $this->ticketRepository->count(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureDashboard(): Dashboard
|
||||||
|
{
|
||||||
|
return Dashboard::new()->setTitle('Abiball Admin Interface');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureMenuItems(): iterable
|
||||||
|
{
|
||||||
|
yield MenuItem::linktoDashboard('Dashboard', 'fa fa-home');
|
||||||
|
yield MenuItem::section('Daten');
|
||||||
|
yield MenuItem::linkToCrud('Kunden', 'fa fa-users', Customer::class);
|
||||||
|
yield MenuItem::linkToCrud('Tickets', 'fa fa-ticket', Ticket::class);
|
||||||
|
yield MenuItem::linkToCrud('Zahlungen', 'fa fa-money', Payment::class);
|
||||||
|
}
|
||||||
|
}
|
13
src/DataObjects/FoodData.php
Normal file
13
src/DataObjects/FoodData.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\DataObjects;
|
||||||
|
|
||||||
|
class FoodData
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $totalMeat = 0,
|
||||||
|
public int $totalVegetarian = 0,
|
||||||
|
public int $totalVegan = 0,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
23
src/DataObjects/LoginData.php
Normal file
23
src/DataObjects/LoginData.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\DataObjects;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
class LoginData
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?string $username = '',
|
||||||
|
public ?string $password = ''
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromRequest(Request $request): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$request->get('_username'),
|
||||||
|
$request->get('_password'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ use Doctrine\Common\Collections\Collection;
|
|||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: CustomerRepository::class)]
|
#[ORM\Entity(repositoryClass: CustomerRepository::class)]
|
||||||
class Customer
|
class Customer implements \Stringable
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
@ -149,4 +149,9 @@ class Customer
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->firstname . ' ' . $this->lastname;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Enum\TicketData;
|
||||||
use App\Repository\PaymentRepository;
|
use App\Repository\PaymentRepository;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
@ -63,4 +64,11 @@ class Payment
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTotal(): float
|
||||||
|
{
|
||||||
|
return $this->customer->getTickets()->reduce(function (float $total, Ticket $ticket) {
|
||||||
|
return $total + TicketData::TICKET_DATA[$ticket->getType()]['price'];
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Enum\TicketData;
|
||||||
use App\Repository\TicketRepository;
|
use App\Repository\TicketRepository;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: TicketRepository::class)]
|
#[ORM\Entity(repositoryClass: TicketRepository::class)]
|
||||||
class Ticket
|
class Ticket implements \Stringable
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
@ -78,4 +79,9 @@ class Ticket
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return TicketData::TICKET_DATA[$this->type]['name'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
18
src/Enum/FoodData.php
Normal file
18
src/Enum/FoodData.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enum;
|
||||||
|
|
||||||
|
class FoodData
|
||||||
|
{
|
||||||
|
public const FOOD_DATA = [
|
||||||
|
1 => 'Mit Fleisch',
|
||||||
|
2 => 'Vegetarisch',
|
||||||
|
3 => 'Vegan'
|
||||||
|
];
|
||||||
|
|
||||||
|
public const TYPES = [
|
||||||
|
'Mit Fleisch' => 1,
|
||||||
|
'Vegetarisch' => 2,
|
||||||
|
'Vegan' => 3,
|
||||||
|
];
|
||||||
|
}
|
@ -22,4 +22,11 @@ class TicketData
|
|||||||
'price' => 0,
|
'price' => 0,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public const TYPES = [
|
||||||
|
'All-Inclusive Ticket' => 1,
|
||||||
|
'After-Show Ticket' => 2,
|
||||||
|
'Kind (6-12 Jahre)' => 3,
|
||||||
|
'Kind (0-6 Jahre)' => 4,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
48
src/Form/TicketType.php
Normal file
48
src/Form/TicketType.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\Ticket;
|
||||||
|
use App\Enum\FoodData;
|
||||||
|
use App\Enum\TicketData;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class TicketType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('type', ChoiceType::class, [
|
||||||
|
'label' => 'Ticket Type',
|
||||||
|
'choices' => TicketData::TYPES,
|
||||||
|
'row_attr' => [
|
||||||
|
'style' => 'flex: 1;',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('foodType', ChoiceType::class, [
|
||||||
|
'label' => 'Ernährung',
|
||||||
|
'choices' => FoodData::TYPES,
|
||||||
|
'row_attr' => [
|
||||||
|
'style' => 'flex: 1;',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('note', TextType::class, [
|
||||||
|
'label' => 'Notiz',
|
||||||
|
'required' => false,
|
||||||
|
'row_attr' => [
|
||||||
|
'style' => 'flex: 2;',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => Ticket::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -15,4 +15,15 @@ class PaymentRepository extends ServiceEntityRepository
|
|||||||
{
|
{
|
||||||
parent::__construct($registry, Payment::class);
|
parent::__construct($registry, Payment::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTotalMoneyMade(): float
|
||||||
|
{
|
||||||
|
$all = $this->findAll();
|
||||||
|
$total = 0.0;
|
||||||
|
foreach ($all as $payment) {
|
||||||
|
$total += $payment->getTotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $total;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Repository;
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\DataObjects\FoodData;
|
||||||
use App\Entity\Ticket;
|
use App\Entity\Ticket;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
@ -15,4 +16,22 @@ class TicketRepository extends ServiceEntityRepository
|
|||||||
{
|
{
|
||||||
parent::__construct($registry, Ticket::class);
|
parent::__construct($registry, Ticket::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFoodData(): FoodData
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('t')
|
||||||
|
->select(
|
||||||
|
'SUM(CASE WHEN t.foodType = 1 THEN 1 ELSE 0 END) as totalMeat',
|
||||||
|
'SUM(CASE WHEN t.foodType = 2 THEN 1 ELSE 0 END) as totalVegetarian',
|
||||||
|
'SUM(CASE WHEN t.foodType = 3 THEN 1 ELSE 0 END) as totalVegan'
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $qb->getQuery()->getSingleResult();
|
||||||
|
|
||||||
|
return new FoodData(
|
||||||
|
(int) $result['totalMeat'],
|
||||||
|
(int) $result['totalVegetarian'],
|
||||||
|
(int) $result['totalVegan']
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
45
src/Security/AdminPanelAuthenticator.php
Normal file
45
src/Security/AdminPanelAuthenticator.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Security;
|
||||||
|
|
||||||
|
use App\DataObjects\LoginData;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
|
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||||
|
|
||||||
|
class AdminPanelAuthenticator extends AbstractAuthenticator
|
||||||
|
{
|
||||||
|
public function supports(Request $request): ?bool
|
||||||
|
{
|
||||||
|
return str_starts_with($request->getRequestUri(), '/admin') && $request->isMethod(Request::METHOD_POST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(Request $request): Passport
|
||||||
|
{
|
||||||
|
$data = LoginData::fromRequest($request);
|
||||||
|
|
||||||
|
if ($request->isMethod(Request::METHOD_POST) && (!$data->password || !$data->username)) {
|
||||||
|
throw new CustomUserMessageAuthenticationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Passport(new UserBadge($data->username), new PasswordCredentials($data->password), [new RememberMeBadge()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||||
|
{
|
||||||
|
return new RedirectResponse('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
31
src/Twig/Ticket.php
Normal file
31
src/Twig/Ticket.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Twig;
|
||||||
|
|
||||||
|
use App\Enum\FoodData;
|
||||||
|
use App\Enum\TicketData;
|
||||||
|
use Twig\Extension\AbstractExtension;
|
||||||
|
use Twig\TwigFilter;
|
||||||
|
use Twig\TwigFunction;
|
||||||
|
|
||||||
|
class Ticket extends AbstractExtension
|
||||||
|
{
|
||||||
|
public function getFilters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigFilter('food', $this->getFoodName(...)),
|
||||||
|
new TwigFilter('ticket', $this->getTicket(...)),
|
||||||
|
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFoodName(int $id): string
|
||||||
|
{
|
||||||
|
return FoodData::FOOD_DATA[$id] ?? 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTicket(int $id): array
|
||||||
|
{
|
||||||
|
return TicketData::TICKET_DATA[$id] ?? ['name' => 'N/A', 'price' => 'N/A'];
|
||||||
|
}
|
||||||
|
}
|
44
symfony.lock
44
symfony.lock
@ -35,6 +35,15 @@
|
|||||||
"migrations/.gitignore"
|
"migrations/.gitignore"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"easycorp/easyadmin-bundle": {
|
||||||
|
"version": "4.24",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "b131e6cbfe1b898a508987851963fff485986285"
|
||||||
|
}
|
||||||
|
},
|
||||||
"php-flasher/flasher-noty-symfony": {
|
"php-flasher/flasher-noty-symfony": {
|
||||||
"version": "v2.1.2"
|
"version": "v2.1.2"
|
||||||
},
|
},
|
||||||
@ -146,6 +155,19 @@
|
|||||||
"config/routes.yaml"
|
"config/routes.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/security-bundle": {
|
||||||
|
"version": "7.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.4",
|
||||||
|
"ref": "2ae08430db28c8eb4476605894296c82a642028f"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/security.yaml",
|
||||||
|
"config/routes/security.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/stimulus-bundle": {
|
"symfony/stimulus-bundle": {
|
||||||
"version": "2.22",
|
"version": "2.22",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
@ -161,6 +183,19 @@
|
|||||||
"assets/controllers/hello_controller.js"
|
"assets/controllers/hello_controller.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/translation": {
|
||||||
|
"version": "7.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.3",
|
||||||
|
"ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/translation.yaml",
|
||||||
|
"translations/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
"symfony/twig-bundle": {
|
"symfony/twig-bundle": {
|
||||||
"version": "7.2",
|
"version": "7.2",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
@ -174,6 +209,15 @@
|
|||||||
"templates/base.html.twig"
|
"templates/base.html.twig"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/uid": {
|
||||||
|
"version": "7.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"symfony/ux-icons": {
|
"symfony/ux-icons": {
|
||||||
"version": "2.22",
|
"version": "2.22",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
31
templates/admin/customer_tickets.html.twig
Normal file
31
templates/admin/customer_tickets.html.twig
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Ernährung</th>
|
||||||
|
<th>Preis</th>
|
||||||
|
<th>Anmerkung</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for ticket in field.value %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ (ticket.type | ticket)['name'] }}</td>
|
||||||
|
<td>{{ ticket.foodType | food }}</td>
|
||||||
|
<td>{{ (ticket.type | ticket)['price'] }}€</td>
|
||||||
|
<td>{{ ticket.note }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ path('admin', { entity: 'App\Entity\Ticket', action: 'detail', id: ticket.id }) }}"
|
||||||
|
class="btn btn-info btn-sm">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center">No Orders Found</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
34
templates/admin/dashboard.html.twig
Normal file
34
templates/admin/dashboard.html.twig
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% extends '@EasyAdmin/page/content.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Ticketwert insgesamt</h5>
|
||||||
|
<p class="card-text">{{ totalMoney }} €</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Essens Daten</h5>
|
||||||
|
<p class="card-text">Mit Fleisch: {{ foodData.totalMeat }}</p>
|
||||||
|
<p class="card-text">Vegetarisch: {{ foodData.totalVegetarian }}</p>
|
||||||
|
<p class="card-text">Vegan: {{ foodData.totalVegan }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Ticket Anzahl</h5>
|
||||||
|
<p class="card-text">{{ totalTickets }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
58
templates/admin/login.html.twig
Normal file
58
templates/admin/login.html.twig
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="min-h-screen flex items-center justify-center bg-[#0a0a0a] px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="w-full max-w-[90%] sm:max-w-md">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h2 class="text-xl sm:text-2xl font-medium text-gray-200">Abiball Admin Panel</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
{{ error.message }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" class="space-y-4 sm:space-y-6" action="{{ path('admin_login') }}">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-medium text-gray-300">Benutzername</label>
|
||||||
|
<div class="mt-1 relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<twig:ux:icon name="mdi:account" class="h-4 w-4 sm:h-5 sm:w-5 text-gray-500" />
|
||||||
|
</div>
|
||||||
|
<input type="text"
|
||||||
|
id="username"
|
||||||
|
name="_username"
|
||||||
|
required
|
||||||
|
class="block w-full pl-9 sm:pl-10 py-2 sm:py-2.5 text-sm sm:text-base bg-[#2a2a2a] border border-[#333333] text-gray-200 rounded-md focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 transition-colors"
|
||||||
|
placeholder="username" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-300">Passwort</label>
|
||||||
|
<div class="mt-1 relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<twig:ux:icon name="mdi:lock" class="h-4 w-4 sm:h-5 sm:w-5 text-gray-500" />
|
||||||
|
</div>
|
||||||
|
<input type="password"
|
||||||
|
id="password"
|
||||||
|
name="_password"
|
||||||
|
required
|
||||||
|
class="block w-full pl-9 sm:pl-10 py-2 sm:py-2.5 text-sm sm:text-base bg-[#2a2a2a] border border-[#333333] text-gray-200 rounded-md focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 transition-colors"
|
||||||
|
placeholder="••••••••" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full flex justify-center py-2 sm:py-2.5 px-4 text-sm sm:text-base bg-orange-500 hover:bg-orange-600 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-[#0a0a0a] focus:ring-orange-500 transition-colors">
|
||||||
|
Anmelden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
0
translations/.gitignore
vendored
Normal file
0
translations/.gitignore
vendored
Normal file
Loading…
x
Reference in New Issue
Block a user