diff --git a/assets/controllers/elements/toggle_visibility_controller.js b/assets/controllers/elements/toggle_visibility_controller.js index 4600dfb2..51c9cb33 100644 --- a/assets/controllers/elements/toggle_visibility_controller.js +++ b/assets/controllers/elements/toggle_visibility_controller.js @@ -7,30 +7,41 @@ export default class extends Controller { }; connect() { - this.readableCheckbox = this.element.querySelector("#readable"); + this.displayCheckbox = this.element.querySelector("#display"); + this.displaySelect = this.element.querySelector("select#display"); - if (!this.readableCheckbox) { - return; + if (this.displayCheckbox) { + this.toggleContainers(this.displayCheckbox.checked); + + this.displayCheckbox.addEventListener("change", (event) => { + this.toggleContainers(event.target.checked); + }); } - // Apply the initial visibility state based on the checkbox being checked or not - this.toggleContainers(this.readableCheckbox.checked); + if (this.displaySelect) { + this.toggleContainers(this.hasDisplaySelectValue()); + + this.displaySelect.addEventListener("change", () => { + this.toggleContainers(this.hasDisplaySelectValue()); + }); + } - // 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). + * Check whether a value was selected in the selectbox + * @returns {boolean} True when a value has not been selected that is not empty */ - toggleContainers(isChecked) { + hasDisplaySelectValue() { + return this.displaySelect && this.displaySelect.value !== ""; + } + + /** + * Hides specified containers if the state is active (checkbox checked or select with value). + * + * @param {boolean} isActive - True when the checkbox is activated or the selectbox has a value. + */ + toggleContainers(isActive) { if (!Array.isArray(this.classesValue) || this.classesValue.length === 0) { return; } @@ -42,11 +53,10 @@ export default class extends Controller { 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" : ""; + element.style.display = isActive ? "none" : ""; }); }); } -} \ No newline at end of file + +} diff --git a/src/Helpers/Assemblies/AssemblyPartAggregator.php b/src/Helpers/Assemblies/AssemblyPartAggregator.php index 94c10257..2346075a 100644 --- a/src/Helpers/Assemblies/AssemblyPartAggregator.php +++ b/src/Helpers/Assemblies/AssemblyPartAggregator.php @@ -24,9 +24,16 @@ namespace App\Helpers\Assemblies; use App\Entity\AssemblySystem\Assembly; use App\Entity\Parts\Part; +use Dompdf\Dompdf; +use Dompdf\Options; +use Twig\Environment; class AssemblyPartAggregator { + public function __construct(private readonly Environment $twig) + { + } + /** * Aggregate the required parts and their total quantities for an assembly. * @@ -80,4 +87,181 @@ class AssemblyPartAggregator } } } + + /** + * Exports a hierarchical Bill of Materials (BOM) for assemblies and parts in a readable format, + * including the multiplier for each part and assembly. + * + * @param Assembly $assembly The root assembly to export. + * @param string $indentationSymbol The symbol used for indentation (e.g., ' '). + * @param int $initialDepth The starting depth for formatting (default: 0). + * @return string Human-readable hierarchical BOM list. + */ + public function exportReadableHierarchy(Assembly $assembly, string $indentationSymbol = ' ', int $initialDepth = 0): string + { + // Start building the hierarchy + $output = ''; + $this->processAssemblyHierarchy($assembly, $initialDepth, 1, $indentationSymbol, $output); + + return $output; + } + + public function exportReadableHierarchyForPdf(array $assemblyHierarchies): string + { + $html = $this->twig->render('assemblies/export_bom_pdf.html.twig', [ + 'assemblies' => $assemblyHierarchies, + ]); + + $options = new Options(); + $options->set('isHtml5ParserEnabled', true); + $options->set('isPhpEnabled', true); + + $dompdf = new Dompdf($options); + $dompdf->loadHtml($html); + $dompdf->setPaper('A4'); + $dompdf->render(); + + $canvas = $dompdf->getCanvas(); + $font = $dompdf->getFontMetrics()->getFont('Arial', 'normal'); + + return $dompdf->output(); + } + + /** + * Recursive method to process assemblies and their parts. + * + * @param Assembly $assembly The current assembly to process. + * @param int $depth The current depth in the hierarchy. + * @param float $parentMultiplier The multiplier inherited from the parent (default is 1 for root). + * @param string $indentationSymbol The symbol used for indentation. + * @param string &$output The cumulative output string. + */ + private function processAssemblyHierarchy(Assembly $assembly, int $depth, float $parentMultiplier, string $indentationSymbol, string &$output): void + { + // Add the current assembly to the output + if ($depth === 0) { + $output .= sprintf( + "%sAssembly: %s [IPN: %s]\n\n", + str_repeat($indentationSymbol, $depth), + $assembly->getName(), + $assembly->getIpn(), + ); + } else { + $output .= sprintf( + "%sAssembly: %s [IPN: %s, Multiplier: %.2f]\n\n", + str_repeat($indentationSymbol, $depth), + $assembly->getName(), + $assembly->getIpn(), + $parentMultiplier + ); + } + + // Gruppiere BOM-Einträge in Kategorien + $parts = []; + $referencedAssemblies = []; + $others = []; + + foreach ($assembly->getBomEntries() as $bomEntry) { + if ($bomEntry->getPart() instanceof Part) { + $parts[] = $bomEntry; + } elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) { + $referencedAssemblies[] = $bomEntry; + } else { + $others[] = $bomEntry; + } + } + + if (!empty($parts)) { + // Process each BOM entry for the current assembly + foreach ($parts as $bomEntry) { + $effectiveQuantity = $bomEntry->getQuantity() * $parentMultiplier; + + $output .= sprintf( + "%sPart: %s [IPN: %s, MPNR: %s, Quantity: %.2f%s, EffectiveQuantity: %.2f]\n", + str_repeat($indentationSymbol, $depth + 1), + $bomEntry->getPart()?->getName(), + $bomEntry->getPart()?->getIpn() ?? '-', + $bomEntry->getPart()?->getManufacturerProductNumber() ?? '-', + $bomEntry->getQuantity(), + $parentMultiplier > 1 ? sprintf(", Multiplier: %.2f", $parentMultiplier) : '', + $effectiveQuantity, + ); + } + + $output .= "\n"; + } + + foreach ($referencedAssemblies as $bomEntry) { + // Add referenced assembly details + $referencedQuantity = $bomEntry->getQuantity() * $parentMultiplier; + + $output .= sprintf( + "%sReferenced Assembly: %s [IPN: %s, Quantity: %.2f%s, EffectiveQuantity: %.2f]\n", + str_repeat($indentationSymbol, $depth + 1), + $bomEntry->getReferencedAssembly()->getName(), + $bomEntry->getReferencedAssembly()->getIpn() ?? '-', + $bomEntry->getQuantity(), + $parentMultiplier > 1 ? sprintf(", Multiplier: %.2f", $parentMultiplier) : '', + $referencedQuantity, + ); + + // Recurse into the referenced assembly + $this->processAssemblyHierarchy( + $bomEntry->getReferencedAssembly(), + $depth + 2, // Increase depth for nested assemblies + $referencedQuantity, // Pass the calculated multiplier + $indentationSymbol, + $output + ); + } + + foreach ($others as $bomEntry) { + $output .= sprintf( + "%sOther: %s [Quantity: %.2f, Multiplier: %.2f]\n", + str_repeat($indentationSymbol, $depth + 1), + $bomEntry->getName(), + $bomEntry->getQuantity(), + $parentMultiplier, + ); + } + } + + public function processAssemblyHierarchyForPdf(Assembly $assembly, int $depth, float $quantity, float $parentMultiplier): array + { + $result = [ + 'name' => $assembly->getName(), + 'ipn' => $assembly->getIpn(), + 'quantity' => $quantity, + 'multiplier' => $depth === 0 ? null : $parentMultiplier, + 'parts' => [], + 'referencedAssemblies' => [], + 'others' => [], + ]; + + foreach ($assembly->getBomEntries() as $bomEntry) { + if ($bomEntry->getPart() instanceof Part) { + $result['parts'][] = [ + 'name' => $bomEntry->getPart()->getName(), + 'ipn' => $bomEntry->getPart()->getIpn(), + 'quantity' => $bomEntry->getQuantity(), + 'effectiveQuantity' => $bomEntry->getQuantity() * $parentMultiplier, + ]; + } elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) { + $result['referencedAssemblies'][] = $this->processAssemblyHierarchyForPdf( + $bomEntry->getReferencedAssembly(), + $depth + 1, + $bomEntry->getQuantity(), + $parentMultiplier * $bomEntry->getQuantity() + ); + } else { + $result['others'][] = [ + 'name' => $bomEntry->getName(), + 'quantity' => $bomEntry->getQuantity(), + 'multiplier' => $parentMultiplier, + ]; + } + } + + return $result; + } } diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 4b5658f1..d1fb6cda 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -81,8 +81,9 @@ class EntityExporter $resolver->setDefault('include_children', false); $resolver->setAllowedTypes('include_children', 'bool'); - $resolver->setDefault('readable', false); - $resolver->setAllowedTypes('readable', 'bool'); + $resolver->setDefault('readableSelect', null); + $resolver->setAllowedValues('readableSelect', [null, 'readable', 'readable_bom']); + } /** @@ -240,7 +241,7 @@ class EntityExporter $entities = [$entities]; } - if ($request->get('readable', false)) { + if ($request->get('readableSelect', false) === 'readable') { // Map entity classes to export functions $entityExportMap = [ AttachmentType::class => fn($entities) => $this->exportReadable($entities, AttachmentType::class), @@ -273,6 +274,23 @@ class EntityExporter $options['format'] = 'csv'; $options['level'] = 'readable'; + } if ($request->get('readableSelect', false) === 'readable_bom') { + $hierarchies = []; + + foreach ($entities as $entity) { + if (!$entity instanceof Assembly) { + throw new InvalidArgumentException('Only assemblies can be exported in readable BOM format'); + } + + $hierarchies[] = $this->assemblyPartAggregator->processAssemblyHierarchyForPdf($entity, 0, 1, 1); + } + + $pdfContent = $this->assemblyPartAggregator->exportReadableHierarchyForPdf($hierarchies); + + $response = new Response($pdfContent); + + $options['format'] = 'pdf'; + $options['level'] = 'readable_bom'; } else { //Do the serialization with the given options $serialized_data = $this->exportEntities($entities, $options); @@ -294,6 +312,7 @@ class EntityExporter 'json' => 'application/json', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xls' => 'application/vnd.ms-excel', + 'pdf' => 'application/pdf', default => 'text/plain', }; $response->headers->set('Content-Type', $content_type); diff --git a/templates/admin/_export_form.html.twig b/templates/admin/_export_form.html.twig index 52751864..4810f67f 100644 --- a/templates/admin/_export_form.html.twig +++ b/templates/admin/_export_form.html.twig @@ -35,13 +35,13 @@
-
-
- - -
+ +
+
@@ -50,4 +50,4 @@
- \ No newline at end of file + diff --git a/templates/assemblies/export_bom_pdf.html.twig b/templates/assemblies/export_bom_pdf.html.twig new file mode 100644 index 00000000..15bf5d88 --- /dev/null +++ b/templates/assemblies/export_bom_pdf.html.twig @@ -0,0 +1,103 @@ + + + + Assembly Hierarchy + + + + + +

Table of Contents

+ + + + + + + + + + + {% for assembly in assemblies %} + + + + + + + {% endfor %} + +
#Assembly NameIPNSection
{{ loop.index }}Assembly: {{ assembly.name }}{% if assembly.ipn != '' %}{{ assembly.ipn }}{% else %}-{% endif %}{{ loop.index + 1 }}
+
+ + +{% for assembly in assemblies %} +
Assembly: {{ assembly.name }}
+ + + + + + + + + + + + {% for part in assembly.parts %} + + + + + + + + {% endfor %} + {% for other in assembly.others %} + + + + + + + + {% endfor %} + {% for referencedAssembly in assembly.referencedAssemblies %} + + + + + + + + {% endfor %} + +
NameIPNQuantityMultiplierEffective Quantity
{{ part.name }}{{ part.ipn }}{{ part.quantity }}{% if assembly.multiplier %}{{ assembly.multiplier }}{% else %}-{% endif %}{{ part.effectiveQuantity }}
{{ other.name }}{{ other.ipn }}{{ other.quantity }}{{ other.multiplier }}{{ other.effectiveQuantity }}
{{ referencedAssembly.name }}{{ referencedAssembly.ipn }}{{ referencedAssembly.quantity }}{{ referencedAssembly.quantity }}
+ + {% for refAssembly in assembly.referencedAssemblies %} + {% include 'assemblies/export_bom_referenced_assembly_pdf.html.twig' with {'assembly': refAssembly} only %} + {% endfor %} + + {% if not loop.last %} +
+ {% endif %} + + +{% endfor %} + + diff --git a/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig b/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig new file mode 100644 index 00000000..b5a1324d --- /dev/null +++ b/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig @@ -0,0 +1,55 @@ +
+
Referenced Assembly: {{ assembly.name }} [IPN: {% if assembly.ipn != '' %}{{ assembly.ipn }}{% else %}-{% endif %}, quantity: {{ assembly.quantity }}]
+ + + + + + + + + + + + + + {% for part in assembly.parts %} + + + + + + + + + {% endfor %} + + {% for other in assembly.others %} + + + + + + + + + {% endfor %} + + {% for referencedAssembly in assembly.referencedAssemblies %} + + + + + + + + + {% endfor %} + +
TypeNameIPNQuantityMultiplierEffective Quantity
Part{{ part.name }}{{ part.ipn }}{{ part.quantity }}{% if assembly.multiplier %}{{ assembly.multiplier }}{% else %}-{% endif %}{{ part.effectiveQuantity }}
Other{{ other.name }}-{{ other.quantity }}{{ other.multiplier }}-
Referenced assembly{{ referencedAssembly.name }}-{{ referencedAssembly.quantity }}{{ referencedAssembly.multiplier }}
+ + + {% for refAssembly in assembly.referencedAssemblies %} + {% include 'assemblies/export_bom_referenced_assembly_pdf.html.twig' with {'assembly': refAssembly} only %} + {% endfor %} +
diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index c5f9000a..ad2d4bb7 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -351,10 +351,22 @@ Exportovat všechny prvky + + + export.readable.label + Čitelný export + + export.readable - Čitelné CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 4518c801..c07d229d 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -351,10 +351,22 @@ Eksportér alle elementer + + + export.readable.label + Læsbar eksport + + export.readable - Læsbar CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 31b5f5e9..7f4f5d33 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1004,10 +1004,22 @@ Subelemente werden beim Löschen nach oben verschoben. Unterelemente auch exportieren + + + export.readable.label + Lesbarer Export + + export.readable - Lesbares CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index fecbac21..b9be2574 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -228,10 +228,22 @@ Εξαγωγή όλων των στοιχείων + + + export.readable.label + Αναγνώσιμη εξαγωγή + + export.readable - Αναγνώσιμο CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 1295c253..7a78c01a 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -351,10 +351,22 @@ Export all elements + + + export.readable.label + Readable Export + + export.readable - Readable CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 0268e83c..8539d631 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -351,10 +351,22 @@ Exportar todos los elementos + + + export.readable.label + Exportación legible + + export.readable - CSV legible + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 44af739d..ae9e9aff 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -320,10 +320,22 @@ Exporter tous les éléments + + + export.readable.label + Export lisible + + export.readable - CSV lisible + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 70ae96f2..e7b012ed 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -351,10 +351,22 @@ Esportare tutti gli elementi + + + export.readable.label + Esporta leggibile + + export.readable - CSV leggibile + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 13ced250..4d92f055 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -320,10 +320,22 @@ すべてエクスポートする + + + export.readable.label + 読みやすいエクスポート + + export.readable - 読みやすいCSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 5d473dc2..63a42c6c 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -351,10 +351,22 @@ Exporteer alle elementen + + + export.readable.label + Leesbare export + + export.readable - Leesbare CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 88ba9a0a..2c77cfea 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -351,10 +351,22 @@ Eksportuj wszystkie elementy + + + export.readable.label + Eksport czytelny + + export.readable - Czytelny CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 4d0b102b..4345c6c9 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -351,10 +351,22 @@ Экспортировать всё + + + export.readable.label + Читаемый экспорт + + export.readable - Читаемый CSV + CSV + + + + + export.readable_bom + PDF diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 6fc40930..34da9e78 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -351,10 +351,22 @@ 导出所有元素 + + + export.readable.label + 可读导出 + + export.readable - 可读的 CSV + CSV + + + + + export.readable_bom + PDF