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( + '%s', + '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( - '%s', - '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) }} + +
+
+
+ +
+
+
+ {% if project.description is not empty %} + {{ project.description|format_markdown }} + {% endif %} +
+ +
+
+
+
+
+
+ + {{ project.name }} +
+
+ + + {% if project.parent %} + {{ project.parent.fullPath }} + {% else %} + - + {% endif %} + +
+
+
+ {% block quick_links %}{% endblock %} + + + {% trans %}entity.edit.btn{% endtrans %} + +
+ + {{ project.lastModified | format_datetime("short") }} + +
+ + {{ project.addedDate | format_datetime("short") }} + +
+
+
+
+
+
+
+ + {{ project.children | length }} +
+
+ + {{ project.bomEntries | length }} +
+
+
+ + {% if project.attachments is not empty %} +
+ {% include "Parts/info/_attachments_info.html.twig" with {"part": project} %} +
+ {% endif %} + + {% if project.parameters is not empty %} +
+ {% for name, parameters in project.groupedParameters %} + {% if name is not empty %}
{{ name }}
{% endif %} + {{ helper.parameters_table(project) }} + {% endfor %} +
+ {% endif %} + + {% if project.comment is not empty %} +
+
+ {{ project.comment|format_markdown }} +
+
+ {% endif %} +
+
+
+
+
+
+
\ No newline at end of file diff --git a/templates/Projects/info.html.twig b/templates/Projects/info.html.twig new file mode 100644 index 00000000..861183af --- /dev/null +++ b/templates/Projects/info.html.twig @@ -0,0 +1,19 @@ +{% extends "base.html.twig" %} + +{% import "components/datatables.macro.html.twig" as datatables %} + +{% block title %} + {% trans %}parts_list.category.title{% endtrans %} {{ project.name }} +{% endblock %} + +{% block content %} + + {% include "Projects/_info_card.html.twig" %} + + {{ datatables.datatable(datatable, 'elements/datatables/datatables', 'projects') }} + + {# {% include "Parts/lists/_action_bar.html.twig" with {'url_options': {'category': entity.iD}} %} + + {% include "Parts/lists/_parts_list.html.twig" %} #} + +{% endblock %} \ No newline at end of file diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 6ee35efc..28cebd1a 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -8693,7 +8693,7 @@ Element 3 - + tfa.provider.webauthn_two_factor_provider Security key @@ -9768,184 +9768,190 @@ Element 3 - + entity.info.parts_count_recursive Number of parts with this element or its subelements - + tools.server_infos.title Server Infos - + permission.preset.read_only Read-Only - + permission.preset.read_only.desc Only allow read operations on data - + permission.preset.all_inherit Inherit all - + permission.preset.all_inherit.desc Set all permissions to Inherit - + permission.preset.all_forbid Forbid all - + permission.preset.all_forbid.desc Set all permissions to Forbid - + permission.preset.all_allow Allow all - + permission.preset.all_allow.desc Set all permissions to allow - + perm.server_infos Server infos - + permission.preset.editor Editor - + permission.preset.editor.desc Allow to change parts and data structures - + permission.preset.admin Admin - + permission.preset.admin.desc Allow administrative actions - + permission.preset.button Apply preset - + perm.attachments.show_private Show private attachments - + perm.attachments.list_attachments Show list of all attachments - + user.edit.permission_success Permission preset applied successfully. Check if the new permissions fit your needs. - + perm.group.data Data - + part_list.action.action.group.needs_review Needs Review - + part_list.action.action.set_needs_review Set Needs Review Status - + part_list.action.action.unset_needs_review Unset Needs Review Status - + part.edit.ipn Internal Part Number (IPN) - + part.ipn.not_defined Not defined - + part.table.ipn IPN - + currency.edit.update_rate Retrieve exchange rate - + currency.edit.exchange_rate_update.unsupported_currency The currency is unsupported by the exchange rate provider. Check your exchange rate provider configuration. - + currency.edit.exchange_rate_update.generic_error Unable to retrieve the exchange rate. Check your exchange rate provider configuration. - + currency.edit.exchange_rate_updated.success Retrieved the exchange rate successfully. + + + project.bom.quantity + BOM Qty. + +