diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php
new file mode 100644
index 00000000..ede384e0
--- /dev/null
+++ b/src/Controller/ProjectController.php
@@ -0,0 +1,61 @@
+.
+ */
+
+namespace App\Controller;
+
+use App\DataTables\ProjectBomEntriesDataTable;
+use App\Entity\ProjectSystem\Project;
+use Omines\DataTablesBundle\DataTableFactory;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Annotation\Route;
+
+/**
+ * @Route("/project")
+ */
+class ProjectController extends AbstractController
+{
+ private DataTableFactory $dataTableFactory;
+
+ public function __construct(DataTableFactory $dataTableFactory)
+ {
+ $this->dataTableFactory = $dataTableFactory;
+ }
+
+ /**
+ * @Route("/{id}", name="project_info")
+ */
+ public function info(Project $project, Request $request)
+ {
+ $this->denyAccessUnlessGranted('read', $project);
+
+ $table = $this->dataTableFactory->createFromType(ProjectBomEntriesDataTable::class, ['project' => $project])
+ ->handleRequest($request);
+
+ if ($table->isCallback()) {
+ return $table->getResponse();
+ }
+
+ return $this->render('Projects/info.html.twig', [
+ 'datatable' => $table,
+ 'project' => $project,
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/src/DataTables/Column/EntityColumn.php b/src/DataTables/Column/EntityColumn.php
index 715e39c4..6d78aac3 100644
--- a/src/DataTables/Column/EntityColumn.php
+++ b/src/DataTables/Column/EntityColumn.php
@@ -65,8 +65,8 @@ class EntityColumn extends AbstractColumn
});
$resolver->setDefault('render', function (Options $options) {
- return function ($value, Part $context) use ($options) {
- /** @var AbstractDBElement|null $entity */
+ return function ($value, $context) use ($options) {
+ /** @var AbstractNamedDBElement|null $entity */
$entity = $this->accessor->getValue($context, $options['property']);
if (null !== $entity) {
@@ -74,7 +74,7 @@ class EntityColumn extends AbstractColumn
return sprintf(
'%s',
$this->urlGenerator->listPartsURL($entity),
- $value
+ $entity->getName()
);
}
diff --git a/src/DataTables/Helpers/PartDataTableHelper.php b/src/DataTables/Helpers/PartDataTableHelper.php
new file mode 100644
index 00000000..37737052
--- /dev/null
+++ b/src/DataTables/Helpers/PartDataTableHelper.php
@@ -0,0 +1,91 @@
+.
+ */
+
+namespace App\DataTables\Helpers;
+
+use App\Entity\Parts\Part;
+use App\Services\Attachments\AttachmentURLGenerator;
+use App\Services\Attachments\PartPreviewGenerator;
+use App\Services\EntityURLGenerator;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+/**
+ * A helper service which contains common code to render columns for part related tables
+ */
+class PartDataTableHelper
+{
+ private PartPreviewGenerator $previewGenerator;
+ private AttachmentURLGenerator $attachmentURLGenerator;
+
+ private TranslatorInterface $translator;
+ private EntityURLGenerator $entityURLGenerator;
+
+ public function __construct(PartPreviewGenerator $previewGenerator, AttachmentURLGenerator $attachmentURLGenerator,
+ EntityURLGenerator $entityURLGenerator, TranslatorInterface $translator)
+ {
+ $this->previewGenerator = $previewGenerator;
+ $this->attachmentURLGenerator = $attachmentURLGenerator;
+ $this->translator = $translator;
+ $this->entityURLGenerator = $entityURLGenerator;
+ }
+
+ public function renderName(Part $context): string
+ {
+ $icon = '';
+
+ //Depending on the part status we show a different icon (the later conditions have higher priority)
+ if ($context->isFavorite()) {
+ $icon = sprintf('', $this->translator->trans('part.favorite.badge'));
+ }
+ if ($context->isNeedsReview()) {
+ $icon = sprintf('', $this->translator->trans('part.needs_review.badge'));
+ }
+
+
+ return sprintf(
+ '%s%s',
+ $this->entityURLGenerator->infoURL($context),
+ $icon,
+ htmlentities($context->getName())
+ );
+ }
+
+ public function renderPicture(Part $context): string
+ {
+ $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context);
+ if (null === $preview_attachment) {
+ return '';
+ }
+
+ $title = htmlspecialchars($preview_attachment->getName());
+ if ($preview_attachment->getFilename()) {
+ $title .= ' ('.htmlspecialchars($preview_attachment->getFilename()).')';
+ }
+
+ return sprintf(
+ '',
+ 'Part image',
+ $this->attachmentURLGenerator->getThumbnailURL($preview_attachment),
+ $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_md'),
+ 'img-fluid hoverpic',
+ $title
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php
index e77cd081..f38215e3 100644
--- a/src/DataTables/PartsDataTable.php
+++ b/src/DataTables/PartsDataTable.php
@@ -34,6 +34,7 @@ use App\DataTables\Column\SIUnitNumberColumn;
use App\DataTables\Column\TagsColumn;
use App\DataTables\Filters\PartFilter;
use App\DataTables\Filters\PartSearchFilter;
+use App\DataTables\Helpers\PartDataTableHelper;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
@@ -63,26 +64,27 @@ final class PartsDataTable implements DataTableTypeInterface
private TranslatorInterface $translator;
private NodesListBuilder $treeBuilder;
private AmountFormatter $amountFormatter;
- private PartPreviewGenerator $previewGenerator;
private AttachmentURLGenerator $attachmentURLGenerator;
private Security $security;
+ private PartDataTableHelper $partDataTableHelper;
+
/**
* @var EntityURLGenerator
*/
private $urlGenerator;
public function __construct(EntityURLGenerator $urlGenerator, TranslatorInterface $translator,
- NodesListBuilder $treeBuilder, AmountFormatter $amountFormatter,
- PartPreviewGenerator $previewGenerator, AttachmentURLGenerator $attachmentURLGenerator, Security $security)
+ NodesListBuilder $treeBuilder, AmountFormatter $amountFormatter,PartDataTableHelper $partDataTableHelper,
+ AttachmentURLGenerator $attachmentURLGenerator, Security $security)
{
$this->urlGenerator = $urlGenerator;
$this->translator = $translator;
$this->treeBuilder = $treeBuilder;
$this->amountFormatter = $amountFormatter;
- $this->previewGenerator = $previewGenerator;
$this->attachmentURLGenerator = $attachmentURLGenerator;
$this->security = $security;
+ $this->partDataTableHelper = $partDataTableHelper;
}
public function configureOptions(OptionsResolver $optionsResolver): void
@@ -122,46 +124,13 @@ final class PartsDataTable implements DataTableTypeInterface
'label' => '',
'className' => 'no-colvis',
'render' => function ($value, Part $context) {
- $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context);
- if (null === $preview_attachment) {
- return '';
- }
-
- $title = htmlspecialchars($preview_attachment->getName());
- if ($preview_attachment->getFilename()) {
- $title .= ' ('.htmlspecialchars($preview_attachment->getFilename()).')';
- }
-
- return sprintf(
- '
',
- 'Part image',
- $this->attachmentURLGenerator->getThumbnailURL($preview_attachment),
- $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_md'),
- 'img-fluid hoverpic',
- $title
- );
+ return $this->partDataTableHelper->renderPicture($context);
},
])
->add('name', TextColumn::class, [
'label' => $this->translator->trans('part.table.name'),
'render' => function ($value, Part $context) {
- $icon = '';
-
- //Depending on the part status we show a different icon (the later conditions have higher priority)
- if ($context->isFavorite()) {
- $icon = sprintf('', $this->translator->trans('part.favorite.badge'));
- }
- if ($context->isNeedsReview()) {
- $icon = sprintf('', $this->translator->trans('part.needs_review.badge'));
- }
-
-
- return sprintf(
- '%s%s',
- $this->urlGenerator->infoURL($context),
- $icon,
- htmlentities($context->getName())
- );
+ return $this->partDataTableHelper->renderName($context);
},
])
->add('id', TextColumn::class, [
diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php
new file mode 100644
index 00000000..50586749
--- /dev/null
+++ b/src/DataTables/ProjectBomEntriesDataTable.php
@@ -0,0 +1,178 @@
+.
+ */
+
+namespace App\DataTables;
+
+use App\DataTables\Column\EntityColumn;
+use App\DataTables\Column\LocaleDateTimeColumn;
+use App\DataTables\Column\MarkdownColumn;
+use App\DataTables\Column\SelectColumn;
+use App\DataTables\Helpers\PartDataTableHelper;
+use App\Entity\Attachments\Attachment;
+use App\Entity\Parts\Part;
+use App\Entity\ProjectSystem\ProjectBOMEntry;
+use App\Services\EntityURLGenerator;
+use App\Services\Formatters\AmountFormatter;
+use Doctrine\ORM\QueryBuilder;
+use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
+use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter;
+use Omines\DataTablesBundle\Column\TextColumn;
+use Omines\DataTablesBundle\DataTable;
+use Omines\DataTablesBundle\DataTableTypeInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+class ProjectBomEntriesDataTable implements DataTableTypeInterface
+{
+ protected TranslatorInterface $translator;
+ protected PartDataTableHelper $partDataTableHelper;
+ protected EntityURLGenerator $entityURLGenerator;
+ protected AmountFormatter $amountFormatter;
+
+ public function __construct(TranslatorInterface $translator, PartDataTableHelper $partDataTableHelper,
+ EntityURLGenerator $entityURLGenerator, AmountFormatter $amountFormatter)
+ {
+ $this->translator = $translator;
+ $this->partDataTableHelper = $partDataTableHelper;
+ $this->entityURLGenerator = $entityURLGenerator;
+ $this->amountFormatter = $amountFormatter;
+ }
+
+
+ public function configure(DataTable $dataTable, array $options)
+ {
+ $dataTable
+ //->add('select', SelectColumn::class)
+ ->add('picture', TextColumn::class, [
+ 'label' => '',
+ 'className' => 'no-colvis',
+ 'render' => function ($value, ProjectBOMEntry $context) {
+ if($context->getPart() === null) {
+ return '';
+ }
+ return $this->partDataTableHelper->renderPicture($context->getPart());
+ },
+ ])
+
+ ->add('id', TextColumn::class, [
+ 'label' => $this->translator->trans('part.table.id'),
+ 'visible' => false,
+ ])
+
+ ->add('quantity', TextColumn::class, [
+ 'label' => $this->translator->trans('project.bom.quantity'),
+ 'className' => 'text-center',
+ 'render' => function ($value, ProjectBOMEntry $context) {
+ //If we have a non-part entry, only show the rounded quantity
+ if ($context->getPart() === null) {
+ return round($context->getQuantity());
+ }
+ //Otherwise use the unit of the part to format the quantity
+ return $this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit());
+ },
+ ])
+
+ ->add('name', TextColumn::class, [
+ 'label' => $this->translator->trans('part.table.name'),
+ 'orderable' => false,
+ 'render' => function ($value, ProjectBOMEntry $context) {
+ if($context->getPart() === null) {
+ return $context->getName();
+ }
+ if($context->getPart() !== null) {
+ $tmp = $this->partDataTableHelper->renderName($context->getPart());
+ if(!empty($context->getName())) {
+ $tmp .= '
'.htmlspecialchars($context->getName()).'';
+ }
+ return $tmp;
+ }
+ },
+ ])
+
+ ->add('description', MarkdownColumn::class, [
+ 'label' => $this->translator->trans('part.table.description'),
+ 'data' => function (ProjectBOMEntry $context) {
+ if($context->getPart() !== null) {
+ return $context->getPart()->getDescription();
+ }
+ //For non-part BOM entries show the comment field
+ return $context->getComment();
+ },
+ ])
+
+
+ ->add('category', EntityColumn::class, [
+ 'label' => $this->translator->trans('part.table.category'),
+ 'property' => 'part.category',
+ ])
+ ->add('footprint', EntityColumn::class, [
+ 'property' => 'part.footprint',
+ 'label' => $this->translator->trans('part.table.footprint'),
+ ])
+
+ ->add('manufacturer', EntityColumn::class, [
+ 'property' => 'part.manufacturer',
+ 'label' => $this->translator->trans('part.table.manufacturer'),
+ ])
+
+ ->add('mountnames', TextColumn::class, [
+
+ ])
+
+
+ ->add('addedDate', LocaleDateTimeColumn::class, [
+ 'label' => $this->translator->trans('part.table.addedDate'),
+ 'visible' => false,
+ ])
+ ->add('lastModified', LocaleDateTimeColumn::class, [
+ 'label' => $this->translator->trans('part.table.lastModified'),
+ 'visible' => false,
+ ])
+ ;
+
+ $dataTable->createAdapter(ORMAdapter::class, [
+ 'entity' => Attachment::class,
+ 'query' => function (QueryBuilder $builder) use ($options): void {
+ $this->getQuery($builder, $options);
+ },
+ 'criteria' => [
+ function (QueryBuilder $builder) use ($options): void {
+ $this->buildCriteria($builder, $options);
+ },
+ new SearchCriteriaProvider(),
+ ],
+ ]);
+ }
+
+ private function getQuery(QueryBuilder $builder, array $options): void
+ {
+ $builder->select('bom_entry')
+ ->addSelect('part')
+ ->from(ProjectBOMEntry::class, 'bom_entry')
+ ->leftJoin('bom_entry.part', 'part')
+ ->where('bom_entry.project = :project')
+ ->setParameter('project', $options['project']);
+ ;
+ }
+
+ private function buildCriteria(QueryBuilder $builder, array $options): void
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php
index 30c6d684..54970b08 100644
--- a/src/Entity/ProjectSystem/Project.php
+++ b/src/Entity/ProjectSystem/Project.php
@@ -52,7 +52,7 @@ class Project extends AbstractStructuralDBElement
protected $parent;
/**
- * @ORM\OneToMany(targetEntity="ProjectBOMEntry", mappedBy="device")
+ * @ORM\OneToMany(targetEntity="ProjectBOMEntry", mappedBy="project")
*/
protected $bom_entries;
diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php
index 0b4104c6..221c77bd 100644
--- a/src/Entity/ProjectSystem/ProjectBOMEntry.php
+++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php
@@ -68,7 +68,7 @@ class ProjectBOMEntry extends AbstractDBElement
* @ORM\ManyToOne(targetEntity="Project", inversedBy="parts")
* @ORM\JoinColumn(name="id_device", referencedColumnName="id")
*/
- protected ?Project $device = null;
+ protected ?Project $project = null;
/**
* @var Part|null The part associated with this
@@ -76,4 +76,116 @@ class ProjectBOMEntry extends AbstractDBElement
* @ORM\JoinColumn(name="id_part", referencedColumnName="id", nullable=true)
*/
protected ?Part $part = null;
+
+ /**
+ * @return float
+ */
+ public function getQuantity(): float
+ {
+ return $this->quantity;
+ }
+
+ /**
+ * @param float $quantity
+ * @return ProjectBOMEntry
+ */
+ public function setQuantity(float $quantity): ProjectBOMEntry
+ {
+ $this->quantity = $quantity;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMountnames(): string
+ {
+ return $this->mountnames;
+ }
+
+ /**
+ * @param string $mountnames
+ * @return ProjectBOMEntry
+ */
+ public function setMountnames(string $mountnames): ProjectBOMEntry
+ {
+ $this->mountnames = $mountnames;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param string $name
+ * @return ProjectBOMEntry
+ */
+ public function setName(string $name): ProjectBOMEntry
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getComment(): string
+ {
+ return $this->comment;
+ }
+
+ /**
+ * @param string $comment
+ * @return ProjectBOMEntry
+ */
+ public function setComment(string $comment): ProjectBOMEntry
+ {
+ $this->comment = $comment;
+ return $this;
+ }
+
+ /**
+ * @return Project|null
+ */
+ public function getProject(): ?Project
+ {
+ return $this->project;
+ }
+
+ /**
+ * @param Project|null $project
+ * @return ProjectBOMEntry
+ */
+ public function setProject(?Project $project): ProjectBOMEntry
+ {
+ $this->project = $project;
+ return $this;
+ }
+
+
+
+ /**
+ * @return Part|null
+ */
+ public function getPart(): ?Part
+ {
+ return $this->part;
+ }
+
+ /**
+ * @param Part|null $part
+ * @return ProjectBOMEntry
+ */
+ public function setPart(?Part $part): ProjectBOMEntry
+ {
+ $this->part = $part;
+ return $this;
+ }
+
+
}
diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php
index 81d87f00..c84ee1c5 100644
--- a/src/Services/EntityURLGenerator.php
+++ b/src/Services/EntityURLGenerator.php
@@ -322,6 +322,8 @@ class EntityURLGenerator
public function listPartsURL(AbstractDBElement $entity): string
{
$map = [
+ Project::class => 'project_info',
+
Category::class => 'part_list_category',
Footprint::class => 'part_list_footprint',
Manufacturer::class => 'part_list_manufacturer',
diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php
index 00deded3..3bba2acf 100644
--- a/src/Services/Trees/TreeViewGenerator.php
+++ b/src/Services/Trees/TreeViewGenerator.php
@@ -110,7 +110,7 @@ class TreeViewGenerator
}
if ($mode === 'devices') {
- $href_type = '';
+ $href_type = 'list_parts';
}
$generic = $this->getGenericTree($class, $parent);
diff --git a/templates/Projects/_info_card.html.twig b/templates/Projects/_info_card.html.twig
new file mode 100644
index 00000000..898aa6b8
--- /dev/null
+++ b/templates/Projects/_info_card.html.twig
@@ -0,0 +1,133 @@
+{% import "helper.twig" as helper %}
+{% import "LabelSystem/dropdown_macro.html.twig" as dropdown %}
+
+{{ helper.breadcrumb_entity_link(project) }}
+
+