WIP: Implement Admin CRUD
This commit is contained in:
@@ -3,8 +3,8 @@ import { Controller } from '@hotwired/stimulus'
|
|||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [ 'form', 'title', 'content' ]
|
static targets = [ 'form', 'title', 'content' ]
|
||||||
|
|
||||||
show({ action, title, content }) {
|
show({ targetUrl, title, content }) {
|
||||||
this.formTarget.action = action
|
this.formTarget.action = targetUrl
|
||||||
this.titleTarget.innerText = title
|
this.titleTarget.innerText = title
|
||||||
this.contentTarget.innerHTML = content
|
this.contentTarget.innerHTML = content
|
||||||
this.element.classList.remove('hidden')
|
this.element.classList.remove('hidden')
|
||||||
|
|||||||
@@ -3,18 +3,11 @@ import { Controller } from '@hotwired/stimulus'
|
|||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static outlets = [ 'confirmation-modal', 'ajax-modal' ]
|
static outlets = [ 'confirmation-modal', 'ajax-modal' ]
|
||||||
|
|
||||||
confirmation({ params: { title, action, content } }) {
|
confirmation({ params: { title, targetUrl, content } }) {
|
||||||
this.confirmationModalOutlet.show({
|
this.confirmationModalOutlet.show({ title, targetUrl, content })
|
||||||
title: title,
|
|
||||||
action: action,
|
|
||||||
content: content,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ajax({ params: { title, url } }) {
|
ajax({ params: { title, url } }) {
|
||||||
this.ajaxModalOutlet.show({
|
this.ajaxModalOutlet.show({ title, url })
|
||||||
title: title,
|
|
||||||
url: url,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,3 @@
|
|||||||
@import "_components.css";
|
@import "_components.css";
|
||||||
|
|
||||||
@import "tailwindcss/utilities";
|
@import "tailwindcss/utilities";
|
||||||
@import "_utilities.css";
|
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
"symfony/yaml": "6.3.*",
|
"symfony/yaml": "6.3.*",
|
||||||
"twig/extra-bundle": "^2.12|^3.0",
|
"twig/extra-bundle": "^2.12|^3.0",
|
||||||
"twig/html-extra": "^3.7",
|
"twig/html-extra": "^3.7",
|
||||||
|
"twig/intl-extra": "^3.7",
|
||||||
"twig/twig": "^2.12|^3.0"
|
"twig/twig": "^2.12|^3.0"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|||||||
Generated
+65
-1
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "2b10ee7954ba92748752ca9e16b1a17a",
|
"content-hash": "05df8d6ad31865116a5ac469c6e03b20",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "doctrine/cache",
|
"name": "doctrine/cache",
|
||||||
@@ -8032,6 +8032,70 @@
|
|||||||
],
|
],
|
||||||
"time": "2023-07-29T15:34:56+00:00"
|
"time": "2023-07-29T15:34:56+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "twig/intl-extra",
|
||||||
|
"version": "v3.7.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/twigphp/intl-extra.git",
|
||||||
|
"reference": "4f4fe572f635534649cc069e1dafe4a8ad63774d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/4f4fe572f635534649cc069e1dafe4a8ad63774d",
|
||||||
|
"reference": "4f4fe572f635534649cc069e1dafe4a8ad63774d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1.3",
|
||||||
|
"symfony/intl": "^5.4|^6.0",
|
||||||
|
"twig/twig": "^2.7|^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/phpunit-bridge": "^5.4|^6.3"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Twig\\Extra\\Intl\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"homepage": "http://fabien.potencier.org",
|
||||||
|
"role": "Lead Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A Twig extension for Intl",
|
||||||
|
"homepage": "https://twig.symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"intl",
|
||||||
|
"twig"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/twigphp/intl-extra/tree/v3.7.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2023-07-29T15:34:56+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "twig/twig",
|
"name": "twig/twig",
|
||||||
"version": "v3.6.1",
|
"version": "v3.6.1",
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ services:
|
|||||||
|
|
||||||
App\Twig\AppRuntime:
|
App\Twig\AppRuntime:
|
||||||
arguments:
|
arguments:
|
||||||
|
$intlExtension: '@twig.extension.intl'
|
||||||
$environment: '%kernel.environment%'
|
$environment: '%kernel.environment%'
|
||||||
|
|
||||||
App\Menu\MenuBuilder:
|
App\Menu\MenuBuilder:
|
||||||
|
|||||||
@@ -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
|
private function createKey(string $username, string $password, string $type): string
|
||||||
{
|
{
|
||||||
$date = (new \DateTimeImmutable())->format('Ymd');
|
$date = (new \DateTimeImmutable())->format('Ymd');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ use App\BusProNet\Model\Country;
|
|||||||
use App\BusProNet\Model\CrmAttribute;
|
use App\BusProNet\Model\CrmAttribute;
|
||||||
use App\BusProNet\Model\CrmAttributesResponse;
|
use App\BusProNet\Model\CrmAttributesResponse;
|
||||||
use App\BusProNet\Model\CrmAttributeGroup;
|
use App\BusProNet\Model\CrmAttributeGroup;
|
||||||
|
use App\BusProNet\Model\Pickup;
|
||||||
|
use App\BusProNet\Model\PickupsResponse;
|
||||||
use App\BusProNet\Model\ProfileResponse;
|
use App\BusProNet\Model\ProfileResponse;
|
||||||
use App\BusProNet\Model\BaseResponse;
|
use App\BusProNet\Model\BaseResponse;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
@@ -28,7 +30,7 @@ class ResponseParser
|
|||||||
public function parseXmlString(string $content): BaseResponse
|
public function parseXmlString(string $content): BaseResponse
|
||||||
{
|
{
|
||||||
$xml = simplexml_load_string($content);
|
$xml = simplexml_load_string($content);
|
||||||
$type = (string) $xml->xpath('satz/@typ')[0];
|
$type = (string)$xml->xpath('satz/@typ')[0];
|
||||||
|
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'HINWEIS':
|
case 'HINWEIS':
|
||||||
@@ -43,6 +45,8 @@ class ResponseParser
|
|||||||
}
|
}
|
||||||
case 'STAMMLAENDER':
|
case 'STAMMLAENDER':
|
||||||
return $this->createCountriesResponse($xml);
|
return $this->createCountriesResponse($xml);
|
||||||
|
case 'STAMMZUSTIEGE':
|
||||||
|
return $this->createPickupsResponse($xml);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ResponseParserException('Unable to parse XML response');
|
throw new ResponseParserException('Unable to parse XML response');
|
||||||
@@ -180,6 +184,28 @@ class ResponseParser
|
|||||||
return $response;
|
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
|
private function resolveOptions(array $options): array
|
||||||
{
|
{
|
||||||
$optionsResolver = new OptionsResolver();
|
$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
@@ -6,6 +6,7 @@ use App\Entity\Traits\BlameableEntity;
|
|||||||
use App\Entity\Traits\TimestampableEntity;
|
use App\Entity\Traits\TimestampableEntity;
|
||||||
use App\Repository\FeeRepository;
|
use App\Repository\FeeRepository;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: FeeRepository::class)]
|
#[ORM\Entity(repositoryClass: FeeRepository::class)]
|
||||||
class Fee implements BlameableEntityInterface, TimestampableEntityInterface
|
class Fee implements BlameableEntityInterface, TimestampableEntityInterface
|
||||||
@@ -19,9 +20,11 @@ class Fee implements BlameableEntityInterface, TimestampableEntityInterface
|
|||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[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)]
|
#[ORM\Column(length: 255)]
|
||||||
|
#[Assert\NotBlank(message: 'Bitte gib die Bezeichnung an')]
|
||||||
private ?string $name = null;
|
private ?string $name = null;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
|
|||||||
@@ -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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ class AppExtension extends AbstractExtension
|
|||||||
new TwigFilter('file_size', [AppRuntime::class, 'formatBytes']),
|
new TwigFilter('file_size', [AppRuntime::class, 'formatBytes']),
|
||||||
new TwigFilter('file_icon', [AppRuntime::class, 'fileIconFilter'], ['is_safe' => ['html']]),
|
new TwigFilter('file_icon', [AppRuntime::class, 'fileIconFilter'], ['is_safe' => ['html']]),
|
||||||
new TwigFilter('date_diff', [AppRuntime::class, 'dateDiffForHumans']),
|
new TwigFilter('date_diff', [AppRuntime::class, 'dateDiffForHumans']),
|
||||||
|
new TwigFilter('format_money', [AppRuntime::class, 'formatMoney']),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ use Carbon\Carbon;
|
|||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Extension\RuntimeExtensionInterface;
|
use Twig\Extension\RuntimeExtensionInterface;
|
||||||
|
use Twig\Extra\Intl\IntlExtension;
|
||||||
|
|
||||||
class AppRuntime implements RuntimeExtensionInterface
|
class AppRuntime implements RuntimeExtensionInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly RequestStack $requestStack,
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly IntlExtension $intlExtension,
|
||||||
private readonly string $environment
|
private readonly string $environment
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -35,6 +37,14 @@ class AppRuntime implements RuntimeExtensionInterface
|
|||||||
return sprintf("%.{$precision}f", $bytes / (1024 ** $factor)).@$size[$factor];
|
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
|
public function renderIcon(Environment $environment, string $icon, string $classes = 'w-6 h-6'): string
|
||||||
{
|
{
|
||||||
return $environment->render('_partials/_icon.html.twig', [
|
return $environment->render('_partials/_icon.html.twig', [
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<form method="post" {{ stimulus_target('confirmation-modal', 'form') }}>
|
<form method="post" {{ stimulus_target('confirmation-modal', 'form') }}>
|
||||||
<div {{ stimulus_target('confirmation-modal', 'content') }}></div>
|
<div {{ stimulus_target('confirmation-modal', 'content') }}></div>
|
||||||
<div class="mt-6 flex justify-between">
|
<div class="mt-6 flex items-center space-x-8">
|
||||||
<button type="submit" class="btn btn--warning">
|
<button type="submit" class="btn">
|
||||||
Ja
|
Ja
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn--secondary" {{ stimulus_action('confirmation-modal', 'hide') }}>
|
<button type="button" class="btn btn--secondary" {{ stimulus_action('confirmation-modal', 'hide') }}>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{{ form_start(form) }}
|
||||||
|
<div class="flex flex-col space-y-4 pb-8">
|
||||||
|
{{ form_row(form.name) }}
|
||||||
|
{{ form_row(form.value) }}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
{{ form_rest(form) }}
|
||||||
|
{{ form_end(form) }}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends 'layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Honorare{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-16">
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<a href="{{ path('app_admin_system_fee_index') }}" class="btn btn--secondary">
|
||||||
|
Abbrechen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<div class="pb-8 max-w-screen-md mx-auto">
|
||||||
|
<h1 class="text-2xl font-bold pb-8">
|
||||||
|
Honorar anlegen
|
||||||
|
</h1>
|
||||||
|
{% include 'admin/system/fee/_form.html.twig' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends 'layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Honorare{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-16">
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<a href="{{ path('app_admin_system_fee_index') }}" class="btn btn--secondary">
|
||||||
|
Abbrechen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<div class="pb-8 max-w-screen-md mx-auto">
|
||||||
|
<h1 class="text-2xl font-bold pb-8">
|
||||||
|
Honorar bearbeiten
|
||||||
|
</h1>
|
||||||
|
{% include 'admin/system/fee/_form.html.twig' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{% extends 'layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Honorare{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-16">
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<a href="{{ path('app_admin_system_fee_create') }}" class="btn">
|
||||||
|
Neu
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<h1 class="text-2xl font-bold pb-8">
|
||||||
|
Honorare
|
||||||
|
</h1>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
{% for fee in fees %}
|
||||||
|
<div class="grid grid-cols-3 items-center py-2 px-4 bg-gray-200 rounded-md">
|
||||||
|
<div>
|
||||||
|
{{ fee.name }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ fee.value|format_money }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2 justify-end">
|
||||||
|
<button type="button"
|
||||||
|
class="text-red-500"
|
||||||
|
{{ stimulus_controller('modal-button', [], [], {'confirmation-modal': '#confirmation-modal'}) }}
|
||||||
|
{{ stimulus_action('modal-button', 'confirmation', null, {
|
||||||
|
'title': 'Bist du sicher?',
|
||||||
|
'content': 'Möchtest du das Honorar wirklich löschen?',
|
||||||
|
'target-url': path('app_admin_system_fee_delete', { 'id': fee.id })
|
||||||
|
}) }}>
|
||||||
|
{{ icon('delete') }}
|
||||||
|
</button>
|
||||||
|
<a href="{{ path('app_admin_system_fee_edit', { 'id': fee.id }) }}">
|
||||||
|
{{ icon('edit') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-sm">Keine Daten...</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -90,3 +90,16 @@
|
|||||||
{{ form_widget(form.children['year']) }}
|
{{ form_widget(form.children['year']) }}
|
||||||
</div>
|
</div>
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|
||||||
|
{%- block money_widget -%}
|
||||||
|
{% set currency_class = 'absolute top-1/2 transform -translate-y-1/2 right-0 mr-2' %}
|
||||||
|
{% if errors|length %}
|
||||||
|
{% set currency_class = currency_class ~ ' text-red-500' %}
|
||||||
|
{% else %}
|
||||||
|
{% set currency_class = currency_class ~ ' text-gray-600' %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="relative">
|
||||||
|
{{ block('form_widget_simple') }}
|
||||||
|
<span class="{{ currency_class }}">€</span>
|
||||||
|
</div>
|
||||||
|
{%- endblock money_widget -%}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Tests\BusProNet;
|
namespace App\Tests\BusProNet;
|
||||||
|
|
||||||
use App\BusProNet\Model\CrmAttributesResponse;
|
use App\BusProNet\Model\CrmAttributesResponse;
|
||||||
|
use App\BusProNet\Model\Pickup;
|
||||||
|
use App\BusProNet\Model\PickupsResponse;
|
||||||
use App\BusProNet\Model\ProfileResponse;
|
use App\BusProNet\Model\ProfileResponse;
|
||||||
use App\BusProNet\ResponseParser;
|
use App\BusProNet\ResponseParser;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@@ -49,6 +51,27 @@ class ResponseParserTest extends TestCase
|
|||||||
$this->assertTrue($response->isTeamer());
|
$this->assertTrue($response->isTeamer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testParseSuccessfulPickupsResponse(): void
|
||||||
|
{
|
||||||
|
$content = '<?xml version="1.0" encoding="utf-8" ?><ergebnis><satz typ="STAMMZUSTIEGE"></satz><zustiege><zustieg id="1" idbuspro="1" code="298"><ort>Karlsruhe</ort><strasse>A5 - Autohof Bruchsal</strasse><art>BUS</art><hausabholung>False</hausabholung><crsbuchbar>True</crsbuchbar><internetbuchbar>True</internetbuchbar></zustieg><zustieg id="2" idbuspro="2" code="2"><ort>Bonn</ort><strasse>Bahnhof</strasse><art>BUS</art> <hausabholung>False</hausabholung><crsbuchbar>True</crsbuchbar><internetbuchbar>True</internetbuchbar></zustieg></zustiege></ergebnis>';
|
||||||
|
|
||||||
|
$parser = $this->getParserInstance();
|
||||||
|
$response = $parser->parseXmlString($content);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(PickupsResponse::class, $response);
|
||||||
|
|
||||||
|
$pickups = $response->getPickups();
|
||||||
|
$this->assertCount(2, $pickups);
|
||||||
|
|
||||||
|
$pickup = reset($pickups);
|
||||||
|
$this->assertInstanceOf(Pickup::class, $pickup);
|
||||||
|
$this->assertEquals(1, $pickup->getId());
|
||||||
|
$this->assertEquals(1, $pickup->getBusProId());
|
||||||
|
$this->assertEquals('298', $pickup->getCode());
|
||||||
|
$this->assertEquals('Karlsruhe', $pickup->getCity());
|
||||||
|
$this->assertEquals('A5 - Autohof Bruchsal', $pickup->getStreet());
|
||||||
|
}
|
||||||
|
|
||||||
private function getParserInstance(): ResponseParser
|
private function getParserInstance(): ResponseParser
|
||||||
{
|
{
|
||||||
return new ResponseParser([
|
return new ResponseParser([
|
||||||
|
|||||||
Reference in New Issue
Block a user