WIP: Implement Admin CRUD

This commit is contained in:
Björn Fromme
2023-09-21 17:51:16 +02:00
parent 2dd8a6de19
commit c3c2e1ae2d
26 changed files with 568 additions and 20 deletions
+35
View File
@@ -208,6 +208,41 @@ class ApiClient
});
}
public function getPickups(): BaseResponse
{
return $this->cache->get('bpn_pickups', function (ItemInterface $item) {
$item->expiresAfter(3600);
$data = [
'anfrage' => [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], 'STAMMZUSTIEGE'),
'satz' => ['@typ' => 'STAMMZUSTIEGE'],
],
];
$body = $this
->serializer
->serialize($data, 'xml');
try {
$response = $this->httpClient->request('GET', $this->config['bpn_url'], [
'query' => [
'operation' => $body,
]
]);
$xml = $response->getContent();
return $this->responseParser->parseXmlString($xml);
} catch (\Throwable $e) {
}
$this->logger->error('API error', ['error' => $e->getMessage()]);
throw new ApiClientException($e->getMessage());
});
}
private function createKey(string $username, string $password, string $type): string
{
$date = (new \DateTimeImmutable())->format('Ymd');
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace App\BusProNet\Model;
class Pickup
{
private ?int $id = null;
private ?int $busProId = null;
private ?string $code = null;
private ?string $city;
private ?string $street;
public function getId(): ?int
{
return $this->id;
}
public function setId(?int $id): static
{
$this->id = $id;
return $this;
}
public function getBusProId(): ?int
{
return $this->busProId;
}
public function setBusProId(?int $busProId): static
{
$this->busProId = $busProId;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(?string $code): static
{
$this->code = $code;
return $this;
}
public function getCity(): ?string
{
return $this->city;
}
public function setCity(?string $city): static
{
$this->city = $city;
return $this;
}
public function getStreet(): ?string
{
return $this->street;
}
public function setStreet(?string $street): static
{
$this->street = $street;
return $this;
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\BusProNet\Model;
class PickupsResponse extends BaseResponse
{
private array $pickups = [];
public function getPickups(): array
{
return $this->pickups;
}
public function setPickups(array $pickups): static
{
$this->pickups = $pickups;
return $this;
}
}
+27 -1
View File
@@ -9,6 +9,8 @@ use App\BusProNet\Model\Country;
use App\BusProNet\Model\CrmAttribute;
use App\BusProNet\Model\CrmAttributesResponse;
use App\BusProNet\Model\CrmAttributeGroup;
use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\PickupsResponse;
use App\BusProNet\Model\ProfileResponse;
use App\BusProNet\Model\BaseResponse;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -28,7 +30,7 @@ class ResponseParser
public function parseXmlString(string $content): BaseResponse
{
$xml = simplexml_load_string($content);
$type = (string) $xml->xpath('satz/@typ')[0];
$type = (string)$xml->xpath('satz/@typ')[0];
switch ($type) {
case 'HINWEIS':
@@ -43,6 +45,8 @@ class ResponseParser
}
case 'STAMMLAENDER':
return $this->createCountriesResponse($xml);
case 'STAMMZUSTIEGE':
return $this->createPickupsResponse($xml);
}
throw new ResponseParserException('Unable to parse XML response');
@@ -180,6 +184,28 @@ class ResponseParser
return $response;
}
public function createPickupsResponse(\SimpleXMLElement $xml): PickupsResponse
{
$pickups = [];
foreach ($xml->xpath('zustiege/zustieg') as $item) {
$pickup = new Pickup();
$pickup
->setId((int)$item->attributes()['id'])
->setBusProId((int)$item->attributes()['idbuspro'])
->setCode((string)$item->attributes()['code'])
->setCity((string)$item->xpath('ort')[0])
->setStreet((string)$item->xpath('strasse')[0])
;
$pickups[] = $pickup;
}
$response = new PickupsResponse();
$response->setPickups($pickups);
return $response;
}
private function resolveOptions(array $options): array
{
$optionsResolver = new OptionsResolver();
@@ -0,0 +1,47 @@
<?php
namespace App\Controller\Admin\System\Fee;
use App\Entity\Fee;
use App\Form\FeeType;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class CreateController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/admin/system/fee/create', name: 'app_admin_system_fee_create')]
#[IsGranted('ROLE_ADMIN')]
public function index(Request $request): Response
{
$fee = new Fee();
$form = $this->createForm(FeeType::class, $fee);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($fee);
$this->entityManager->flush();
$this->addFlash('success', 'Das Honorar wurde angelegt');
$this->logger->info('Create fee', [
'fee' => $fee->getName(),
]);
return $this->redirectToRoute('app_admin_system_fee_index');
}
return $this->render('admin/system/fee/create.html.twig', [
'form' => $form
]);
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Controller\Admin\System\Fee;
use App\Entity\Fee;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class DeleteController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/admin/system/fee/delete/{id}', name: 'app_admin_system_fee_delete', methods: ['POST'])]
#[IsGranted('ROLE_ADMIN')]
public function index(Fee $fee): Response
{
$this->entityManager->remove($fee);
$this->entityManager->flush();
$this->addFlash('success', 'Das Honorar wurde gelöscht');
$this->logger->info('Delete fee', [
'fee' => $fee->getName(),
]);
return $this->redirectToRoute('app_admin_system_fee_index');
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Controller\Admin\System\Fee;
use App\Entity\Fee;
use App\Form\FeeType;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class EditController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/admin/system/fee/edit/{id}', name: 'app_admin_system_fee_edit')]
#[IsGranted('ROLE_ADMIN')]
public function index(Fee $fee, Request $request): Response
{
$form = $this->createForm(FeeType::class, $fee);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($fee);
$this->entityManager->flush();
$this->addFlash('success', 'Das Honorar wurde aktualisiert');
$this->logger->info('Update fee', [
'fee' => $fee->getName(),
]);
return $this->redirectToRoute('app_admin_system_fee_index');
}
return $this->render('admin/system/fee/edit.html.twig', [
'form' => $form
]);
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Controller\Admin\System\Fee;
use App\Repository\FeeRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class IndexController extends AbstractController
{
public function __construct(private readonly FeeRepository $feeRepository)
{}
#[Route('/admin/system/fee', name: 'app_admin_system_fee_index')]
#[IsGranted('ROLE_ADMIN')]
public function index(): Response
{
$fees = $this->feeRepository->findBy([], ['name' => 'ASC']);
return $this->render('admin/system/fee/index.html.twig', [
'fees' => $fees,
]);
}
}
+4 -1
View File
@@ -6,6 +6,7 @@ use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Repository\FeeRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: FeeRepository::class)]
class Fee implements BlameableEntityInterface, TimestampableEntityInterface
@@ -19,9 +20,11 @@ class Fee implements BlameableEntityInterface, TimestampableEntityInterface
private ?int $id = null;
#[ORM\Column]
private ?int $value = null;
#[Assert\NotNull(message: 'Bitte gib den Wert an')]
private ?int $value = null; // Stored as int, divide by 100!
#[ORM\Column(length: 255)]
#[Assert\NotBlank(message: 'Bitte gib die Bezeichnung an')]
private ?string $name = null;
public function getId(): ?int
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Form;
use App\Entity\Fee;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FeeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'Bezeichnung',
])
->add('value', MoneyType::class, [
'label' => 'Wert',
'invalid_message' => 'Bitte gib einen gültigen Geldbetrag ein',
'divisor' => 100,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Fee::class,
]);
}
}
+1
View File
@@ -14,6 +14,7 @@ class AppExtension extends AbstractExtension
new TwigFilter('file_size', [AppRuntime::class, 'formatBytes']),
new TwigFilter('file_icon', [AppRuntime::class, 'fileIconFilter'], ['is_safe' => ['html']]),
new TwigFilter('date_diff', [AppRuntime::class, 'dateDiffForHumans']),
new TwigFilter('format_money', [AppRuntime::class, 'formatMoney']),
];
}
+10
View File
@@ -6,11 +6,13 @@ use Carbon\Carbon;
use Symfony\Component\HttpFoundation\RequestStack;
use Twig\Environment;
use Twig\Extension\RuntimeExtensionInterface;
use Twig\Extra\Intl\IntlExtension;
class AppRuntime implements RuntimeExtensionInterface
{
public function __construct(
private readonly RequestStack $requestStack,
private readonly IntlExtension $intlExtension,
private readonly string $environment
) {
}
@@ -35,6 +37,14 @@ class AppRuntime implements RuntimeExtensionInterface
return sprintf("%.{$precision}f", $bytes / (1024 ** $factor)).@$size[$factor];
}
public function formatMoney(int $amount): string
{
// Amounts are stored as integers so divide by 100 first
$amount = $amount / 100;
return $this->intlExtension->formatCurrency($amount, 'EUR');
}
public function renderIcon(Environment $environment, string $icon, string $classes = 'w-6 h-6'): string
{
return $environment->render('_partials/_icon.html.twig', [