From c79fc47c1ee7e4b292baf154bb9a45daec8edea3 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 8 Sep 2025 13:32:34 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=BCge=20Option=20f=C3=BCr=20lesbares=20CSV?= =?UTF-8?q?=20beim=20Export=20hinzu=20(APS-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elements/toggle_visibility_controller.js | 52 ++++ .../AssemblySystem/AssemblyBOMEntry.php | 6 +- .../ImportExportSystem/EntityExporter.php | 291 +++++++++++++++++- templates/admin/_export_form.html.twig | 19 +- translations/messages.cs.xlf | 6 + translations/messages.da.xlf | 6 + translations/messages.de.xlf | 6 + translations/messages.el.xlf | 6 + translations/messages.en.xlf | 6 + translations/messages.es.xlf | 6 + translations/messages.fr.xlf | 10 +- translations/messages.it.xlf | 6 + translations/messages.ja.xlf | 6 + translations/messages.nl.xlf | 6 + translations/messages.pl.xlf | 6 + translations/messages.ru.xlf | 6 + translations/messages.zh.xlf | 6 + 17 files changed, 434 insertions(+), 16 deletions(-) create mode 100644 assets/controllers/elements/toggle_visibility_controller.js diff --git a/assets/controllers/elements/toggle_visibility_controller.js b/assets/controllers/elements/toggle_visibility_controller.js new file mode 100644 index 00000000..4600dfb2 --- /dev/null +++ b/assets/controllers/elements/toggle_visibility_controller.js @@ -0,0 +1,52 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + + static values = { + classes: Array + }; + + connect() { + this.readableCheckbox = this.element.querySelector("#readable"); + + if (!this.readableCheckbox) { + return; + } + + // Apply the initial visibility state based on the checkbox being checked or not + this.toggleContainers(this.readableCheckbox.checked); + + // Add a change event listener to the 'readable' checkbox + this.readableCheckbox.addEventListener("change", (event) => { + // Toggle container visibility when the checkbox value changes + this.toggleContainers(event.target.checked); + }); + } + + /** + * Toggles the visibility of containers based on the checkbox state. + * Hides specified containers if the checkbox is checked and shows them otherwise. + * + * @param {boolean} isChecked - The current state of the checkbox: + * true if checked (hide elements), false if unchecked (show them). + */ + toggleContainers(isChecked) { + if (!Array.isArray(this.classesValue) || this.classesValue.length === 0) { + return; + } + + this.classesValue.forEach((cssClass) => { + const elements = document.querySelectorAll(`.${cssClass}`); + + if (!elements.length) { + return; + } + + // Update the visibility for each selected element + elements.forEach((element) => { + // If the checkbox is checked, hide the container; otherwise, show it + element.style.display = isChecked ? "none" : ""; + }); + }); + } +} \ No newline at end of file diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 7d54fe68..9bca209d 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -124,7 +124,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt */ #[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'bom_entries')] #[ORM\JoinColumn(name: 'id_assembly', nullable: true)] - #[Groups(['bom_entry:read', 'bom_entry:write', ])] + #[Groups(['bom_entry:read', 'bom_entry:write'])] protected ?Assembly $assembly = null; /** @@ -146,7 +146,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt #[AssemblyInvalidBomEntry] #[ORM\ManyToOne(targetEntity: Assembly::class)] #[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')] - #[Groups(['bom_entry:read', 'bom_entry:write', ])] + #[Groups(['bom_entry:read', 'bom_entry:write'])] protected ?Assembly $referencedAssembly = null; /** @@ -158,7 +158,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt )] #[ORM\ManyToOne(targetEntity: Project::class)] #[ORM\JoinColumn(name: 'id_project', nullable: true)] - #[Groups(['bom_entry:read', 'bom_entry:write', ])] + #[Groups(['bom_entry:read', 'bom_entry:write'])] protected ?Project $project = null; /** diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 70feb8e6..56b5743f 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -22,8 +22,19 @@ declare(strict_types=1); namespace App\Services\ImportExportSystem; +use App\Entity\AssemblySystem\Assembly; +use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\LabelSystem\LabelProfile; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Currency; +use App\Entity\ProjectSystem\Project; use App\Helpers\FilenameSanatizer; use App\Serializer\APIPlatform\SkippableItemNormalizer; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -65,6 +76,9 @@ class EntityExporter $resolver->setDefault('include_children', false); $resolver->setAllowedTypes('include_children', 'bool'); + + $resolver->setDefault('readable', false); + $resolver->setAllowedTypes('readable', 'bool'); } /** @@ -222,15 +236,50 @@ class EntityExporter $entities = [$entities]; } - //Do the serialization with the given options - $serialized_data = $this->exportEntities($entities, $options); + if ($request->get('readable', false)) { + // Map entity classes to export functions + $entityExportMap = [ + AttachmentType::class => fn($entities) => $this->exportReadable($entities, AttachmentType::class), + Category::class => fn($entities) => $this->exportReadable($entities, Category::class), + Project::class => fn($entities) => $this->exportReadable($entities, Project::class), + Assembly::class => fn($entities) => $this->exportReadable($entities, Assembly::class), + Supplier::class => fn($entities) => $this->exportReadable($entities, Supplier::class), + Manufacturer::class => fn($entities) => $this->exportReadable($entities, Manufacturer::class), + StorageLocation::class => fn($entities) => $this->exportReadable($entities, StorageLocation::class), + Footprint::class => fn($entities) => $this->exportReadable($entities, Footprint::class), + Currency::class => fn($entities) => $this->exportReadable($entities, Currency::class), + MeasurementUnit::class => fn($entities) => $this->exportReadable($entities, MeasurementUnit::class), + LabelProfile::class => fn($entities) => $this->exportReadable($entities, LabelProfile::class, false), + ]; - $response = new Response($serialized_data); + // Determine the type of the entity + $type = null; + foreach ($entities as $entity) { + $entityClass = get_class($entity); + if (isset($entityExportMap[$entityClass])) { + $type = $entityClass; + break; + } + } - //Resolve the format - $optionsResolver = new OptionsResolver(); - $this->configureOptions($optionsResolver); - $options = $optionsResolver->resolve($options); + // Generate the response + $response = isset($entityExportMap[$type]) + ? new Response($entityExportMap[$type]($entities)) + : new Response(''); + + $options['format'] = 'csv'; + $options['level'] = 'readable'; + } else { + //Do the serialization with the given options + $serialized_data = $this->exportEntities($entities, $options); + + $response = new Response($serialized_data); + + //Resolve the format + $optionsResolver = new OptionsResolver(); + $this->configureOptions($optionsResolver); + $options = $optionsResolver->resolve($options); + } //Determine the content type for the response @@ -277,4 +326,232 @@ class EntityExporter return $response; } + + /** + * Exports data for multiple entity types in a readable CSV format. + * + * @param array $entities The entities to export. + * @param string $type The type of entities ('category', 'project', 'assembly', 'attachmentType', 'supplier'). + * @return string The generated CSV content as a string. + */ + public function exportReadable(array $entities, string $type, bool $isHierarchical = true): string + { + //Define headers and entity-specific processing logic + $defaultProcessEntity = fn($entity, $depth) => [ + 'Id' => $entity->getId(), + 'ParentId' => $entity->getParent()?->getId() ?? '', + 'NameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'Name' => $entity->getName(), + 'FullName' => $this->getFullName($entity), + ]; + + $config = [ + AttachmentType::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Category::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Project::class => [ + 'header' => [ + 'Id', 'ParentId', 'Type', 'ProjectNameHierarchical', 'ProjectName', 'ProjectFullName', + 'BomQuantity', 'BomPartId', 'BomPartIpn', 'BomPartName', 'BomName', 'BomPartDescription', 'BomMountNames' + ], + 'processEntity' => fn($entity, $depth) => [ + 'ProjectId' => $entity->getId(), + 'ParentProjectId' => $entity->getParent()?->getId() ?? '', + 'Type' => 'project', + 'ProjectNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'ProjectName' => $entity->getName(), + 'ProjectFullName' => $this->getFullName($entity), + 'BomQuantity' => '-', + 'BomPartId' => '-', + 'BomPartIpn' => '-', + 'BomPartName' => '-', + 'BomName' => '-', + 'BomPartDescription' => '-', + 'BomMountNames' => '-', + ], + 'processBomEntries' => fn($entity, $depth) => array_map(fn($bomEntry) => [ + 'Id' => $entity->getId(), + 'ParentId' => '', + 'Type' => 'project_bom_entry', + 'ProjectNameHierarchical' => str_repeat('--', $depth) . '> ' . $entity->getName(), + 'ProjectName' => $entity->getName(), + 'ProjectFullName' => $this->getFullName($entity), + 'BomQuantity' => $bomEntry->getQuantity() ?? '', + 'BomPartId' => $bomEntry->getPart()?->getId() ?? '', + 'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '', + 'BomPartName' => $bomEntry->getPart()?->getName() ?? '', + 'BomName' => $bomEntry->getName() ?? '', + 'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '', + 'BomMountNames' => $bomEntry->getMountNames(), + ], $entity->getBomEntries()->toArray()), + ], + Assembly::class => [ + 'header' => [ + 'Id', 'ParentId', 'Type', 'AssemblyIpn', 'AssemblyNameHierarchical', 'AssemblyName', + 'AssemblyFullName', 'BomQuantity', 'BomPartId', 'BomPartIpn', 'BomPartName', 'BomName', 'BomPartDescription', + 'BomMountNames', 'BomReferencedAssemblyId', 'BomReferencedAssemblyIpn', 'BomReferencedAssemblyFullName' + ], + 'processEntity' => fn($entity, $depth) => [ + 'Id' => $entity->getId(), + 'ParentId' => $entity->getParent()?->getId() ?? '', + 'Type' => 'assembly', + 'AssemblyIpn' => $entity->getIpn(), + 'AssemblyNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(), + 'AssemblyName' => $entity->getName(), + 'AssemblyFullName' => $this->getFullName($entity), + 'BomQuantity' => '-', + 'BomPartId' => '-', + 'BomPartIpn' => '-', + 'BomPartName' => '-', + 'BomName' => '-', + 'BomPartDescription' => '-', + 'BomMountNames' => '-', + 'BomReferencedAssemblyId' => '-', + 'BomReferencedAssemblyIpn' => '-', + 'BomReferencedAssemblyFullName' => '-', + ], + 'processBomEntries' => fn($entity, $depth) => array_map(fn($bomEntry) => [ + 'Id' => $entity->getId(), + 'ParentId' => '', + 'Type' => 'assembly_bom_entry', + 'AssemblyIpn' => $entity->getIpn(), + 'AssemblyNameHierarchical' => str_repeat('--', $depth) . '> ' . $entity->getName(), + 'AssemblyName' => $entity->getName(), + 'AssemblyFullName' => $this->getFullName($entity), + 'BomQuantity' => $bomEntry->getQuantity() ?? '', + 'BomPartId' => $bomEntry->getPart()?->getId() ?? '', + 'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '', + 'BomPartName' => $bomEntry->getPart()?->getName() ?? '', + 'BomName' => $bomEntry->getName() ?? '', + 'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '', + 'BomMountNames' => $bomEntry->getMountNames(), + 'BomReferencedAssemblyId' => $bomEntry->getReferencedAssembly()?->getId() ?? '', + 'BomReferencedAssemblyIpn' => $bomEntry->getReferencedAssembly()?->getIpn() ?? '', + 'BomReferencedAssemblyFullName' => $this->getFullName($bomEntry->getReferencedAssembly() ?? null), + ], $entity->getBomEntries()->toArray()), + ], + Supplier::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Manufacturer::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + StorageLocation::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Footprint::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + Currency::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + MeasurementUnit::class => [ + 'header' => ['Id', 'ParentId', 'NameHierarchical', 'Name', 'FullName'], + 'processEntity' => $defaultProcessEntity, + ], + LabelProfile::class => [ + 'header' => ['Id', 'SupportedElement', 'Name'], + 'processEntity' => fn(LabelProfile $entity, $depth) => [ + 'Id' => $entity->getId(), + 'SupportedElement' => $entity->getOptions()->getSupportedElement()->name, + 'Name' => $entity->getName(), + ], + ], + ]; + + //Get configuration for the entity type + $entityConfig = $config[$type] ?? null; + + if (!$entityConfig) { + return ''; + } + + //Initialize CSV data with the header + $csvData = []; + $csvData[] = $entityConfig['header']; + + $relevantEntities = $entities; + + if ($isHierarchical) { + //Filter root entities (those without parents) + $relevantEntities = array_filter($entities, fn($entity) => $entity->getParent() === null); + + if (count($relevantEntities) === 0 && count($entities) > 0) { + //If no root entities are found, then we need to add all entities + + $relevantEntities = $entities; + } + } + + //Sort root entities alphabetically by `name` + usort($relevantEntities, fn($a, $b) => strnatcasecmp($a->getName(), $b->getName())); + + //Recursive function to process an entity and its children + $processEntity = function ($entity, &$csvData, $depth = 0) use (&$processEntity, $entityConfig, $isHierarchical) { + //Add main entity data to CSV + $csvData[] = $entityConfig['processEntity']($entity, $depth); + + //Process BOM entries if applicable + if (isset($entityConfig['processBomEntries'])) { + $bomRows = $entityConfig['processBomEntries']($entity, $depth); + foreach ($bomRows as $bomRow) { + $csvData[] = $bomRow; + } + } + + if ($isHierarchical) { + //Retrieve children, sort alphabetically, then process them + $children = $entity->getChildren()->toArray(); + usort($children, fn($a, $b) => strnatcasecmp($a->getName(), $b->getName())); + foreach ($children as $childEntity) { + $processEntity($childEntity, $csvData, $depth + 1); + } + } + }; + + //Start processing with root entities + foreach ($relevantEntities as $rootEntity) { + $processEntity($rootEntity, $csvData); + } + + //Generate CSV string + $output = ''; + foreach ($csvData as $line) { + $output .= implode(';', $line) . "\n"; // Use a semicolon as the delimiter + } + + return $output; + } + + /** + * Constructs the full hierarchical name of an object by traversing + * through its parent objects and concatenating their names using + * a specified separator. + * + * @param AttachmentType|Category|Project|Assembly|Supplier|Manufacturer|StorageLocation|Footprint|Currency|MeasurementUnit|LabelProfile|null $object The object whose full name is to be constructed. If null, the result will be an empty string. + * @param string $separator The string used to separate the names of the objects in the full hierarchy. + * + * @return string The full hierarchical name constructed by concatenating the names of the object and its parents. + */ + private function getFullName(AttachmentType|Category|Project|Assembly|Supplier|Manufacturer|StorageLocation|Footprint|Currency|MeasurementUnit|LabelProfile|null $object, string $separator = '->'): string + { + $fullNameParts = []; + + while ($object !== null) { + array_unshift($fullNameParts, $object->getName()); + $object = $object->getParent(); + } + + return implode($separator, $fullNameParts); + } } diff --git a/templates/admin/_export_form.html.twig b/templates/admin/_export_form.html.twig index 07b00d43..52751864 100644 --- a/templates/admin/_export_form.html.twig +++ b/templates/admin/_export_form.html.twig @@ -1,6 +1,6 @@ -
+ -
+
@@ -23,7 +23,7 @@
-
+
@@ -34,6 +34,17 @@
+
+
+
+ + +
+
+
+
diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 89edfb80..21b1416e 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -351,6 +351,12 @@ Exportovat všechny prvky + + + export.readable + Čitelné CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 8f28ef08..f4a3881d 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -351,6 +351,12 @@ Eksportér alle elementer + + + export.readable + Læsbar CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 67c5edd2..51252368 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1010,6 +1010,12 @@ Subelemente werden beim Löschen nach oben verschoben. Unterelemente auch exportieren + + + export.readable + Lesbares CSV + + Part-DB1\templates\AdminPages\_export_form.html.twig:39 diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 98630c48..ccbed9fb 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -228,6 +228,12 @@ Εξαγωγή όλων των στοιχείων + + + export.readable + Αναγνώσιμο CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 3ae166bf..4ff8eddd 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -351,6 +351,12 @@ Export all elements + + + export.readable + Readable CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 8b48e442..999fd71a 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -351,6 +351,12 @@ Exportar todos los elementos + + + export.readable + CSV legible + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index ee1d6ffe..b5ed6a27 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -320,6 +320,12 @@ Exporter tous les éléments + + + export.readable + CSV lisible + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 @@ -9822,13 +9828,13 @@ exemple de ville - + assembly.bom_import.template.csv.exptected_columns Colonnes possibles : - + assembly.bom_import.template.csv.table diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index ac134405..951a1bd3 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -351,6 +351,12 @@ Esportare tutti gli elementi + + + export.readable + CSV leggibile + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index e892af9c..101afd62 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -320,6 +320,12 @@ すべてエクスポートする + + + export.readable + 読みやすいCSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index b24e14b9..ebf67d39 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -351,6 +351,12 @@ Exporteer alle elementen + + + export.readable + Leesbare CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index a09e3ac3..d9d4eac2 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -351,6 +351,12 @@ Eksportuj wszystkie elementy + + + export.readable + Czytelny CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 8d6a0ba1..3589e4e1 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -351,6 +351,12 @@ Экспортировать всё + + + export.readable + Читаемый CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185 diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index f257f9a2..a3e44de6 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -351,6 +351,12 @@ 导出所有元素 + + + export.readable + 可读的 CSV + + Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:185