diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 4e9abbb0..192cb497 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -42,6 +42,7 @@ declare(strict_types=1); namespace App\Controller; +use App\DataTables\LogDataTable; use App\Entity\Parts\Category; use App\Entity\Parts\Part; use App\Exceptions\AttachmentDownloadException; @@ -49,9 +50,11 @@ use App\Form\Part\PartBaseType; use App\Services\Attachments\AttachmentManager; use App\Services\Attachments\AttachmentSubmitHandler; use App\Services\Attachments\PartPreviewGenerator; +use App\Services\LogSystem\HistoryHelper; use App\Services\LogSystem\TimeTravel; use App\Services\PricedetailHelper; use Doctrine\ORM\EntityManagerInterface; +use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -82,11 +85,23 @@ class PartController extends AbstractController * * @param Part $part * @return Response + * @throws \Exception */ - public function show(Part $part, TimeTravel $timeTravel, ?string $timestamp = null): Response + public function show(Part $part, Request $request, TimeTravel $timeTravel, HistoryHelper $historyHelper, + DataTableFactory $dataTable, ?string $timestamp = null): Response { $this->denyAccessUnlessGranted('read', $part); + $table = $dataTable->createFromType(LogDataTable::class, [ + 'filter_elements' => $historyHelper->getAssociatedElements($part), + 'mode' => 'element_history' + ], ['pageLength' => 10]) + ->handleRequest($request); + + if ($table->isCallback()) { + return $table->getResponse(); + } + $timeTravel_timestamp = null; if ($timestamp !== null) { //If the timestamp only contains numbers interpret it as unix timestamp @@ -103,6 +118,7 @@ class PartController extends AbstractController 'Parts/info/show_part_info.html.twig', [ 'part' => $part, + 'datatable' => $table, 'attachment_helper' => $this->attachmentManager, 'pricedetail_helper' => $this->pricedetailHelper, 'pictures' => $this->partPreviewGenerator->getPreviewAttachments($part), @@ -123,7 +139,7 @@ class PartController extends AbstractController * @return Response */ public function edit(Part $part, Request $request, EntityManagerInterface $em, TranslatorInterface $translator, - AttachmentSubmitHandler $attachmentSubmitHandler): Response + AttachmentSubmitHandler $attachmentSubmitHandler): Response { $this->denyAccessUnlessGranted('edit', $part); @@ -160,11 +176,11 @@ class PartController extends AbstractController } return $this->render('Parts/edit/edit_part_info.html.twig', - [ - 'part' => $part, - 'form' => $form->createView(), - 'attachment_helper' => $this->attachmentManager, - ]); + [ + 'part' => $part, + 'form' => $form->createView(), + 'attachment_helper' => $this->attachmentManager, + ]); } /** @@ -204,7 +220,7 @@ class PartController extends AbstractController * @return Response */ public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator, - AttachmentManager $attachmentHelper, AttachmentSubmitHandler $attachmentSubmitHandler): Response + AttachmentManager $attachmentHelper, AttachmentSubmitHandler $attachmentSubmitHandler): Response { $new_part = new Part(); @@ -253,11 +269,11 @@ class PartController extends AbstractController } return $this->render('Parts/edit/new_part.html.twig', - [ - 'part' => $new_part, - 'form' => $form->createView(), - 'attachment_helper' => $attachmentHelper, - ]); + [ + 'part' => $new_part, + 'form' => $form->createView(), + 'attachment_helper' => $attachmentHelper, + ]); } /** @@ -289,9 +305,9 @@ class PartController extends AbstractController } return $this->render('Parts/edit/new_part.html.twig', - [ - 'part' => $new_part, - 'form' => $form->createView(), - ]); + [ + 'part' => $new_part, + 'form' => $form->createView(), + ]); } } diff --git a/src/DataTables/Column/IconLinkColumn.php b/src/DataTables/Column/IconLinkColumn.php new file mode 100644 index 00000000..66a19035 --- /dev/null +++ b/src/DataTables/Column/IconLinkColumn.php @@ -0,0 +1,108 @@ +. + */ + +namespace App\DataTables\Column; + + +use Omines\DataTablesBundle\Column\AbstractColumn; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class IconLinkColumn extends AbstractColumn +{ + + /** + * @inheritDoc + */ + public function normalize($value) + { + return $value; + } + + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + $resolver->setDefaults([ + 'icon' => 'fas fa-fw fa-edit', + 'title' => null, + 'href' => null, + ]); + + $resolver->setAllowedTypes('title', ['null', 'string', 'callable']); + $resolver->setAllowedTypes('icon', ['null', 'string', 'callable']); + $resolver->setAllowedTypes('href', ['null', 'string', 'callable']); + } + + public function render($value, $context) + { + $href = $this->getHref($value, $context); + $icon = $this->getIcon($value, $context); + $title = $this->getTitle($value, $context); + + if ($href !== null) { + return sprintf( + '', + $href, + $title, + $icon + ); + } + + return ""; + } + + protected function getHref($value, $context): ?string + { + $provider = $this->options['href']; + if (is_string($provider)) { + return $provider; + } + if (is_callable($provider)) { + return call_user_func($provider, $value, $context); + } + + return null; + } + + protected function getIcon($value, $context): ?string + { + $provider = $this->options['icon']; + if (is_string($provider)) { + return $provider; + } + if (is_callable($provider)) { + return call_user_func($provider, $value, $context); + } + + return null; + } + + protected function getTitle($value, $context): ?string + { + $provider = $this->options['title']; + if (is_string($provider)) { + return $provider; + } + if (is_callable($provider)) { + return call_user_func($provider, $value, $context); + } + + return null; + } +} \ No newline at end of file diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php index 145fbef8..2688a8d7 100644 --- a/src/DataTables/LogDataTable.php +++ b/src/DataTables/LogDataTable.php @@ -42,36 +42,63 @@ declare(strict_types=1); namespace App\DataTables; +use App\DataTables\Column\IconLinkColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\LogEntryExtraColumn; use App\DataTables\Column\LogEntryTargetColumn; +use App\Entity\Base\AbstractDBElement; +use App\Entity\Contracts\TimeTravelInterface; use App\Entity\LogSystem\AbstractLogEntry; +use App\Exceptions\EntityNotSupportedException; use App\Services\ElementTypeNameGenerator; +use App\Services\EntityURLGenerator; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; use Omines\DataTablesBundle\Column\TextColumn; use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTableTypeInterface; use Psr\Log\LogLevel; +use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Flex\Options; class LogDataTable implements DataTableTypeInterface { protected $elementTypeNameGenerator; protected $translator; protected $urlGenerator; + protected $entityURLGenerator; + protected $logRepo; public function __construct(ElementTypeNameGenerator $elementTypeNameGenerator, TranslatorInterface $translator, - UrlGeneratorInterface $urlGenerator) + UrlGeneratorInterface $urlGenerator, EntityURLGenerator $entityURLGenerator, EntityManagerInterface $entityManager) { $this->elementTypeNameGenerator = $elementTypeNameGenerator; $this->translator = $translator; $this->urlGenerator = $urlGenerator; + $this->entityURLGenerator = $entityURLGenerator; + $this->logRepo = $entityManager->getRepository(AbstractLogEntry::class); + } + + public function configureOptions(OptionsResolver $optionsResolver) + { + $optionsResolver->setDefaults([ + 'mode' => 'system_log', + 'filter_elements' => [], + ]); + + $optionsResolver->setAllowedValues('mode', ['system_log', 'element_history']); } public function configure(DataTable $dataTable, array $options): void { + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + $options = $resolver->resolve($options); + + $dataTable->add('symbol', TextColumn::class, [ 'label' => '', 'render' => function ($value, AbstractLogEntry $context) { @@ -138,6 +165,7 @@ class LogDataTable implements DataTableTypeInterface $dataTable->add('level', TextColumn::class, [ 'label' => $this->translator->trans('log.level'), + 'visible' => $options['mode'] === 'system_log', 'propertyPath' => 'levelString', 'render' => function (string $value, AbstractLogEntry $context) { return $value; @@ -178,21 +206,51 @@ class LogDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('log.extra'), ]); + $dataTable->add('timeTravel', IconLinkColumn::class,[ + 'label' => '', + 'icon' => 'fas fa-fw fa-eye', + 'href' => function ($value, AbstractLogEntry $context) { + if ( + $context instanceof TimeTravelInterface + && $context->hasOldDataInformations() + ) { + try { + $target = $this->logRepo->getTargetElement($context); + $str = $this->entityURLGenerator->timeTravelURL($target, $context->getTimestamp()); + return $str; + } catch (EntityNotSupportedException $exception) { + return null; + } + } + return null; + } + ]); + $dataTable->addOrderBy('timestamp', DataTable::SORT_DESCENDING); $dataTable->createAdapter(ORMAdapter::class, [ 'entity' => AbstractLogEntry::class, - 'query' => function (QueryBuilder $builder): void { - $this->getQuery($builder); + 'query' => function (QueryBuilder $builder) use ($options): void { + $this->getQuery($builder, $options); }, ]); } - protected function getQuery(QueryBuilder $builder): void + protected function getQuery(QueryBuilder $builder, array $options): void { $builder->distinct()->select('log') ->addSelect('user') ->from(AbstractLogEntry::class, 'log') ->leftJoin('log.user', 'user'); + + if (!empty($options['filter_elements'])) { + foreach ($options['filter_elements'] as $element) { + /** @var AbstractDBElement $element */ + + $target_type = AbstractLogEntry::targetTypeClassToID(get_class($element)); + $target_id = $element->getID(); + $builder->orWhere("log.target_type = $target_type AND log.target_id = $target_id"); + } + } } } diff --git a/src/Repository/LogEntryRepository.php b/src/Repository/LogEntryRepository.php index 4c31e1b8..35d531d4 100644 --- a/src/Repository/LogEntryRepository.php +++ b/src/Repository/LogEntryRepository.php @@ -90,7 +90,7 @@ class LogEntryRepository extends EntityRepository ->where('log INSTANCE OF ' . ElementEditedLogEntry::class) ->andWhere('log.target_type = :target_type') ->andWhere('log.target_id = :target_id') - ->andWhere('log.timestamp >= :until') + ->andWhere('log.timestamp > :until') ->orderBy('log.timestamp', 'DESC'); $qb->setParameters([ diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index 879afc20..f42c68b4 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -44,6 +44,7 @@ namespace App\Services; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; +use App\Entity\Attachments\PartAttachment; use App\Entity\Base\AbstractDBElement; use App\Entity\Devices\Device; use App\Entity\Parts\Category; @@ -51,9 +52,12 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; use App\Entity\Parts\Storelocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\PriceInformations\Pricedetail; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use App\Exceptions\EntityNotSupportedException; @@ -120,6 +124,49 @@ class EntityURLGenerator throw new InvalidArgumentException('Method is not supported!'); } + /** + * Gets the URL to view the given element at a given timestamp + * @param $entity + * @param \DateTime $dateTime + * @return string + */ + public function timeTravelURL($entity, \DateTime $dateTime): string + { + if ($entity instanceof Part) { + return $this->urlGenerator->generate('part_info', [ + 'id' => $entity->getID(), + 'timestamp' => $dateTime->getTimestamp() + ]); + } + if ($entity instanceof PartLot) { + return $this->urlGenerator->generate('part_info', [ + 'id' => $entity->getPart()->getID(), + 'timestamp' => $dateTime->getTimestamp() + ]); + } + if ($entity instanceof PartAttachment) { + return $this->urlGenerator->generate('part_info', [ + 'id' => $entity->getElement()->getID(), + 'timestamp' => $dateTime->getTimestamp() + ]); + } + if ($entity instanceof Orderdetail) { + return $this->urlGenerator->generate('part_info', [ + 'id' => $entity->getPart()->getID(), + 'timestamp' => $dateTime->getTimestamp() + ]); + } + if ($entity instanceof Pricedetail) { + return $this->urlGenerator->generate('part_info', [ + 'id' => $entity->getOrderdetail()->getPart()->getID(), + 'timestamp' => $dateTime->getTimestamp() + ]); + } + + //Otherwise throw an error + throw new EntityNotSupportedException('The given entity is not supported yet!'); + } + public function viewURL($entity): string { if ($entity instanceof Attachment) { diff --git a/src/Services/LogSystem/HistoryHelper.php b/src/Services/LogSystem/HistoryHelper.php new file mode 100644 index 00000000..d029c272 --- /dev/null +++ b/src/Services/LogSystem/HistoryHelper.php @@ -0,0 +1,57 @@ +. + */ + +namespace App\Services\LogSystem; + + +use App\Entity\Base\AbstractDBElement; +use App\Entity\Parts\Part; + +class HistoryHelper +{ + public function __construct() + { + + } + + /** + * Returns an array containing all elements that are associated with the argument. + * The returned array contains the given element. + * @param AbstractDBElement $element + * @return array + */ + public function getAssociatedElements(AbstractDBElement $element): array + { + $array = [$element]; + if ($element instanceof Part) { + $array = array_merge( + $array, + $element->getAttachments()->toArray(), + $element->getPartLots()->toArray(), + $element->getOrderdetails()->toArray() + ); + foreach ($element->getOrderdetails() as $orderdetail) { + $array = array_merge($array, $orderdetail->getPricedetails()->toArray()); + } + } + + return $array; + } +} \ No newline at end of file diff --git a/templates/Parts/info/_history.html.twig b/templates/Parts/info/_history.html.twig new file mode 100644 index 00000000..b4fad04d --- /dev/null +++ b/templates/Parts/info/_history.html.twig @@ -0,0 +1,3 @@ +