<?php
declare(strict_types=1);
namespace App\Admin;
use App\Entity\Reserve;
use App\Entity\Transaction;
use App\Entity\TransactionCategory;
use App\Entity\Type\DirectionType;
use Doctrine\ORM\QueryBuilder;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Form\Type\ModelType;
use Sonata\AdminBundle\Show\ShowMapper;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
final class TransactionAdmin extends AbstractAdmin
{
protected $datagridValues = [
'_sort_order' => 'DESC',
'_sort_by' => 'id'
];
/**
* Super admin sees all transactions.
* Other admins see only their own, and never reserve-cancellation (OUT) rows —
* those are internal balance adjustments when an active rental is cancelled.
*/
public function createQuery($context = 'list')
{
/** @var QueryBuilder $query */
$query = parent::createQuery($context);
if (!$this->isSuperAdmin()) {
$alias = $query->getRootAliases()[0];
$query
->andWhere(sprintf('%s.admin = :currentAdmin', $alias))
// Hide automatic "cancel active rental" reverse transactions
->andWhere(sprintf(
'NOT (%s.direction = :reserveCancelDirection AND %s.targetEntity = :reserveEntity)',
$alias,
$alias
))
->setParameter('currentAdmin', $this->getCurrentUser())
->setParameter('reserveCancelDirection', DirectionType::OUT)
->setParameter('reserveEntity', Reserve::class);
}
return $query;
}
/**
* Prevent direct URL access to someone else's or hidden reverse-rental transactions.
*
* @param string $action
* @param Transaction|null $object
*/
public function hasAccess($action, $object = null): bool
{
if (!parent::hasAccess($action, $object)) {
return false;
}
if (
$object instanceof Transaction
&& !$this->isSuperAdmin()
&& in_array($action, ['edit', 'show', 'delete'], true)
) {
if ($this->isReserveCancellationTransaction($object)) {
return false;
}
return $this->isOwnedByCurrentUser($object);
}
return true;
}
protected function configureDatagridFilters(DatagridMapper $datagridMapper): void
{
$datagridMapper
->add('id')
->add('amount', null, ['label' => 'Сумма'])
->add('direction', null, [
'label' => 'Приход/Расход',
], ChoiceType::class, [
'choices' => array_flip(DirectionType::RU_VALUES),
])
->add('category', null, ['label' => 'Категория'])
;
if ($this->isSuperAdmin()) {
$datagridMapper->add('admin', null, ['label' => 'Автор']);
}
}
protected function configureListFields(ListMapper $listMapper): void
{
$listMapper->add('amount', null, [
'template' => 'admin/Transaction/list_amount.html.twig',
'label' => 'Сумма'
]);
$listMapper->add('direction', null, [
'template' => 'admin/Transaction/list_direction.html.twig',
'label' => 'Приход/Расход'
]);
$listMapper->add('description', null, ['label' => 'Описание']);
$listMapper->add('category', null, ['label' => 'Категория']);
$listMapper->add('date', null, ['label' => 'Дата транзакции']);
$listMapper->add('createdAt', null, ['label' => 'Внесено в систему']);
if ($this->isSuperAdmin()) {
$listMapper->add('admin', null, [
'template' => 'admin/Transaction/list_admin.html.twig',
'label' => 'Автор'
]);
}
$listMapper->add('_action', null, [
'actions' => [
'show' => [],
'edit' => [],
],
]);
}
protected function configureFormFields(FormMapper $formMapper): void
{
$formMapper->add('direction', ChoiceType::class, [
'choices' => array_flip(DirectionType::RU_VALUES),
'required' => true,
'label' => 'Приход/Расход',
]);
$formMapper->add('amount', null, [
'label' => 'Сумма',
'help' => 'Сумма в BYN',
]);
$formMapper->add('category', ModelType::class, [
'class' => TransactionCategory::class,
'required' => false,
'btn_add' => 'Добавить категорию',
'label' => 'Категория',
]);
$formMapper->add('description', null, [
'label' => 'Описание',
'required' => false,
]);
$formMapper->add('date', null, [
'label' => 'Дата транзакции',
'required' => false,
'help' => 'Можно задать любую дату для проводок задним числом. Если не указана — будет использована дата создания.',
]);
}
protected function configureShowFields(ShowMapper $showMapper): void
{
$showMapper
->add('id')
->add('amount', null, ['label' => 'Сумма'])
->add('direction', 'choice', [
'label' => 'Приход/Расход',
'choices' => DirectionType::RU_VALUES,
])
->add('category', null, ['label' => 'Категория'])
->add('description', null, ['label' => 'Описание'])
->add('date', null, ['label' => 'Дата транзакции'])
->add('createdAt', null, ['label' => 'Внесено'])
;
if ($this->isSuperAdmin()) {
$showMapper->add('admin', null, ['label' => 'Автор']);
}
}
/**
* @param Transaction $object
*/
public function prePersist($object): void
{
$object->setAdmin($this->getCurrentUser());
// Если дата транзакции не указана вручную — используем текущий момент
if ($object->getDate() === null) {
$object->setDate($object->getCreatedAt() ?? new \DateTime());
}
}
public function preUpdate($object): void
{
// При редактировании тоже страхуемся
if ($object->getDate() === null) {
$object->setDate($object->getCreatedAt() ?? new \DateTime());
}
}
private function isSuperAdmin(): bool
{
$authorizationChecker = $this->getContainer()->get('security.authorization_checker');
return $authorizationChecker->isGranted('ROLE_SUPER_ADMIN');
}
private function isOwnedByCurrentUser(Transaction $transaction): bool
{
$author = $transaction->getAdmin();
if ($author === null) {
return false;
}
return $author->getId() === $this->getCurrentUser()->getId();
}
/**
* OUT transactions linked to Reserve = reverse on cancel of active rental.
*/
private function isReserveCancellationTransaction(Transaction $transaction): bool
{
return (int) $transaction->getDirection() === DirectionType::OUT
&& $transaction->getTargetEntity() === Reserve::class;
}
}