diff --git a/assets/controllers/elements/ckeditor_controller.js b/assets/controllers/elements/ckeditor_controller.js index 62a48b15..7f55dd5c 100644 --- a/assets/controllers/elements/ckeditor_controller.js +++ b/assets/controllers/elements/ckeditor_controller.js @@ -78,6 +78,15 @@ export default class extends Controller { editor_div.classList.add(...new_classes.split(",")); } + // Automatic synchronization of source input + editor.model.document.on("change:data", () => { + editor.updateSourceElement(); + + // Dispatch the input event for further treatment + const event = new Event("input"); + this.element.dispatchEvent(event); + }); + //This return is important! Otherwise we get mysterious errors in the console //See: https://github.com/ckeditor/ckeditor5/issues/5897#issuecomment-628471302 return editor; diff --git a/assets/controllers/elements/ipn_suggestion_controller.js b/assets/controllers/elements/ipn_suggestion_controller.js index 088c07b3..e7289a91 100644 --- a/assets/controllers/elements/ipn_suggestion_controller.js +++ b/assets/controllers/elements/ipn_suggestion_controller.js @@ -6,6 +6,7 @@ export default class extends Controller { static values = { partId: Number, partCategoryId: Number, + partDescription: String, suggestions: Object, commonSectionHeader: String, // Dynamic header for common Prefixes partIncrementHeader: String, // Dynamic header for new possible part increment @@ -15,6 +16,7 @@ export default class extends Controller { connect() { this.configureAutocomplete(); this.watchCategoryChanges(); + this.watchDescriptionChanges(); } templates = { @@ -92,34 +94,6 @@ export default class extends Controller { const panelLayout = document.createElement("div"); panelLayout.classList.add("aa-PanelLayout", "aa-Panel--scrollable"); - // Section for common prefixes - if (commonPrefixes.length) { - const commonSection = document.createElement("section"); - commonSection.classList.add("aa-Source"); - - const commonSectionHeader = this.templates.commonSectionHeader({ - title: commonHeader, - html: String.raw, - }); - commonSection.innerHTML += commonSectionHeader; - - const commonList = document.createElement("ul"); - commonList.classList.add("aa-List"); - commonList.setAttribute("role", "listbox"); - - commonPrefixes.forEach((prefix) => { - const itemHTML = this.templates.item({ - suggestion: prefix.title, - description: prefix.description, - html: String.raw, - }); - commonList.innerHTML += itemHTML; - }); - - commonSection.appendChild(commonList); - panelLayout.appendChild(commonSection); - } - // Section for prefixes part increment if (prefixesPartIncrement.length) { const partIncrementSection = document.createElement("section"); @@ -148,6 +122,34 @@ export default class extends Controller { panelLayout.appendChild(partIncrementSection); } + // Section for common prefixes + if (commonPrefixes.length) { + const commonSection = document.createElement("section"); + commonSection.classList.add("aa-Source"); + + const commonSectionHeader = this.templates.commonSectionHeader({ + title: commonHeader, + html: String.raw, + }); + commonSection.innerHTML += commonSectionHeader; + + const commonList = document.createElement("ul"); + commonList.classList.add("aa-List"); + commonList.setAttribute("role", "listbox"); + + commonPrefixes.forEach((prefix) => { + const itemHTML = this.templates.item({ + suggestion: prefix.title, + description: prefix.description, + html: String.raw, + }); + commonList.innerHTML += itemHTML; + }); + + commonSection.appendChild(commonList); + panelLayout.appendChild(commonSection); + } + panel.appendChild(panelLayout); inputField.parentNode.appendChild(panel); @@ -176,25 +178,48 @@ export default class extends Controller { watchCategoryChanges() { const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]'); + const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]'); this.previousCategoryId = Number(this.partCategoryIdValue); if (categoryField) { categoryField.addEventListener("change", () => { const categoryId = Number(categoryField.value); + const description = String(descriptionField.value); // Check whether the category has changed compared to the previous ID if (categoryId !== this.previousCategoryId) { - this.fetchNewSuggestions(categoryId); + this.fetchNewSuggestions(categoryId, description); this.previousCategoryId = categoryId; } }); } } - fetchNewSuggestions(categoryId) { + watchDescriptionChanges() { + const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]'); + const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]'); + this.previousDescription = String(this.partDescriptionValue); + + if (descriptionField) { + descriptionField.addEventListener("input", () => { + const categoryId = Number(categoryField.value); + const description = String(descriptionField.value); + + // Check whether the description has changed compared to the previous one + if (description !== this.previousDescription) { + this.fetchNewSuggestions(categoryId, description); + this.previousDescription = description; + } + }); + } + } + + fetchNewSuggestions(categoryId, description) { const baseUrl = this.suggestUrlValue; const partId = this.partIdValue; - const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}`; + const truncatedDescription = description.length > 150 ? description.substring(0, 150) : description; + const encodedDescription = this.base64EncodeUtf8(truncatedDescription); + const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}&description=${encodedDescription}`; fetch(url, { method: "GET", @@ -217,4 +242,9 @@ export default class extends Controller { console.error("Errors when loading the new IPN-suggestions:", error); }); }; + + base64EncodeUtf8(text) { + const utf8Bytes = new TextEncoder().encode(text); + return btoa(String.fromCharCode(...utf8Bytes)); + }; } \ No newline at end of file diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 92ad8b86..0c7a24e3 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -451,7 +451,7 @@ final class PartController extends AbstractController $template, [ 'part' => $new_part, - 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, $this->autocompletePartDigits), + 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, base64_encode($data->getDescription()), $this->autocompletePartDigits), 'form' => $form, 'merge_old_name' => $merge_infos['tname_before'] ?? null, 'merge_other' => $merge_infos['other_part'] ?? null, diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 76dbf3d0..f1e83d21 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -197,6 +197,7 @@ class TypeaheadController extends AbstractController $partId = null; } $categoryId = $request->query->getInt('categoryId'); + $description = $request->query->getString('description'); /** @var Part $part */ $part = $partId !== null ? $entityManager->getRepository(Part::class)->find($partId) : new Part(); @@ -206,7 +207,7 @@ class TypeaheadController extends AbstractController $clonedPart->setCategory($category); $partRepository = $entityManager->getRepository(Part::class); - $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $this->autocompletePartDigits); + $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, $this->autocompletePartDigits); return new JsonResponse($ipnSuggestions); } diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index 06639bf3..c493f12b 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -86,6 +86,7 @@ class PartBaseType extends AbstractType 'attr' => [ 'placeholder' => 'part.edit.description.placeholder', 'rows' => 2, + 'data-ipn-suggestion' => 'descriptionField', ], ]) ->add('minAmount', SIUnitType::class, [ diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index cdba4f77..69361553 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -109,10 +109,30 @@ class PartRepository extends NamedDBElementRepository return $qb->getQuery()->getResult(); } - public function autoCompleteIpn(Part $part, int $autocompletePartDigits): array + /** + * Provides IPN (Internal Part Number) suggestions for a given part based on its category, description, + * and configured autocomplete digit length. + * + * This function generates suggestions for common prefixes and incremented prefixes based on + * the part's current category and its hierarchy. If the part is unsaved, a default "n.a." prefix is returned. + * + * @param Part $part The part for which autocomplete suggestions are generated. + * @param string $description Base64-encoded description to assist in generating suggestions. + * @param int $autocompletePartDigits The number of digits used in autocomplete increments. + * + * @return array An associative array containing the following keys: + * - 'commonPrefixes': List of common prefixes found for the part. + * - 'prefixesPartIncrement': Increments for the generated prefixes, including hierarchical prefixes. + */ + public function autoCompleteIpn(Part $part, string $description, int $autocompletePartDigits): array { $category = $part->getCategory(); $ipnSuggestions = ['commonPrefixes' => [], 'prefixesPartIncrement' => []]; + $description = base64_decode($description); + + if (strlen($description) > 150) { + $description = substr($description, 0, 150); + } // Validate the category and ensure it's an instance of Category if ($category instanceof Category) { @@ -127,6 +147,22 @@ class PartRepository extends NamedDBElementRepository 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category') ]; + $suggestionByDescription = $this->getIpnSuggestByDescription($description); + + if ($suggestionByDescription !== null && $suggestionByDescription !== $part->getIpn() && $part->getIpn() !== null && $part->getIpn() !== '') { + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $part->getIpn(), + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.current-increment') + ]; + } + + if ($suggestionByDescription !== null) { + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $suggestionByDescription, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.increment') + ]; + } + $ipnSuggestions['prefixesPartIncrement'][] = [ 'title' => $currentPath . '-' . $increment, 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category.increment') @@ -165,7 +201,62 @@ class PartRepository extends NamedDBElementRepository return $ipnSuggestions; } - public function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $autocompletePartDigits): string + /** + * Suggests the next IPN (Internal Part Number) based on the provided part description. + * + * Searches for parts with similar descriptions and retrieves their existing IPNs to calculate the next suggestion. + * Returns null if the description is empty or no suggestion can be generated. + * + * @param string $description The part description to search for. + * + * @return string|null The suggested IPN, or null if no suggestion is possible. + * + * @throws NonUniqueResultException + */ + public function getIpnSuggestByDescription(string $description): ?string + { + if ($description === '') { + return null; + } + + $qb = $this->createQueryBuilder('part'); + + $qb->select('part') + ->where('part.description LIKE :descriptionPattern') + ->setParameter('descriptionPattern', $description.'%') + ->orderBy('part.id', 'ASC'); + + $partsBySameDescription = $qb->getQuery()->getResult(); + $givenIpnsWithSameDescription = []; + + foreach ($partsBySameDescription as $part) { + if ($part->getIpn() === null || $part->getIpn() === '') { + continue; + } + + $givenIpnsWithSameDescription[] = $part->getIpn(); + } + + return $this->getNextIpnSuggestion($givenIpnsWithSameDescription); + } + + /** + * Generates the next possible increment for a part within a given category, while ensuring uniqueness. + * + * This method calculates the next available increment for a part's identifier (`ipn`) based on the current path + * and the number of digits specified for the autocomplete feature. It ensures that the generated identifier + * aligns with the expected length and does not conflict with already existing identifiers in the same category. + * + * @param string $currentPath The base path or prefix for the part's identifier. + * @param Part $currentPart The part entity for which the increment is being generated. + * @param int $autocompletePartDigits The number of digits reserved for the increment. + * + * @return string|null The next possible increment as a zero-padded string, or null if it cannot be generated. + * + * @throws NonUniqueResultException If the query returns non-unique results. + * @throws NoResultException If the query fails to return a result. + */ + private function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $autocompletePartDigits): ?string { $qb = $this->createQueryBuilder('part'); @@ -213,4 +304,41 @@ class PartRepository extends NamedDBElementRepository return str_pad((string) $nextIncrement, $autocompletePartDigits, '0', STR_PAD_LEFT); } + + /** + * Generates the next IPN suggestion based on the maximum numeric suffix found in the given IPNs. + * + * The new IPN is constructed using the base format of the first provided IPN, + * incremented by the next free numeric suffix. If no base IPNs are found, + * returns null. + * + * @param array $givenIpns List of IPNs to analyze. + * + * @return string|null The next suggested IPN, or null if no base IPNs can be derived. + */ + private function getNextIpnSuggestion(array $givenIpns): ?string { + $maxSuffix = 0; + + foreach ($givenIpns as $ipn) { + // Check whether the IPN contains a suffix "_ " + if (preg_match('/_(\d+)$/', $ipn, $matches)) { + $suffix = (int)$matches[1]; + if ($suffix > $maxSuffix) { + $maxSuffix = $suffix; // Höchste Nummer speichern + } + } + } + + // Find the basic format (the IPN without suffix) from the first IPN + $baseIpn = $givenIpns[0] ?? ''; + $baseIpn = preg_replace('/_\d+$/', '', $baseIpn); // Entferne vorhandene "_" + + if ($baseIpn === '') { + return null; + } + + // Generate next free possible IPN + return $baseIpn . '_' . ($maxSuffix + 1); + } + } diff --git a/templates/parts/edit/_advanced.html.twig b/templates/parts/edit/_advanced.html.twig index 4dd91dd1..de31786c 100644 --- a/templates/parts/edit/_advanced.html.twig +++ b/templates/parts/edit/_advanced.html.twig @@ -4,6 +4,7 @@
Návrhy s číselnými přírůstky částí + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuální specifikace IPN pro součást + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Další možná specifikace IPN na základě identického popisu součásti + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 0510fa16..14a6e31a 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -1862,6 +1862,18 @@ Underelementer vil blive flyttet opad. Forslag med numeriske deleforøgelser + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuel IPN-specifikation for delen + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Næste mulige IPN-specifikation baseret på en identisk delebeskrivelse + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 34421bf8..a726c3cd 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1853,6 +1853,18 @@ Subelemente werden beim Löschen nach oben verschoben. Vorschläge mit numerischen Teil-Inkrement + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuelle IPN-Angabe des Bauteils + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Nächstmögliche IPN-Angabe auf Basis der identischen Bauteil-Beschreibung + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 02703ca7..3473c0d1 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1565,6 +1565,18 @@ Προτάσεις με αριθμητικές αυξήσεις μερών + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Τρέχουσα προδιαγραφή IPN του εξαρτήματος + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Επόμενη δυνατή προδιαγραφή IPN βάσει της ίδιας περιγραφής εξαρτήματος + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 3dbbcf1c..a231458b 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -1854,6 +1854,18 @@ Sub elements will be moved upwards. Suggestions with numeric part increment + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Current IPN specification of the part + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Next possible IPN specification based on an identical part description + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 0a2be1bd..1282e8a3 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -1854,6 +1854,18 @@ Subelementos serán desplazados hacia arriba. Sugerencias con incrementos numéricos de partes + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Especificación actual de IPN de la pieza + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Siguiente especificación de IPN posible basada en una descripción idéntica de la pieza + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 0e3aee01..d1aed017 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -1832,6 +1832,18 @@ Show/Hide sidebar Propositions avec incréments numériques de parties + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Spécification IPN actuelle pour la pièce + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Prochaine spécification IPN possible basée sur une description identique de la pièce + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 41b20e49..16a839d8 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -1854,6 +1854,18 @@ I sub elementi saranno spostati verso l'alto. Suggerimenti con incrementi numerici delle parti + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Specifica IPN attuale per il pezzo + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Prossima specifica IPN possibile basata su una descrizione identica del pezzo + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index b9e5ddf9..86306ab3 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -1832,6 +1832,18 @@ パーツの数値インクリメントを含む提案 + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + 部品の現在のIPN仕様 + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + 同じ部品説明に基づく次の可能なIPN仕様 + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 99cbd706..e97b3649 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -754,6 +754,18 @@ Suggesties met numerieke verhogingen van onderdelen + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Huidige IPN-specificatie voor het onderdeel + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Volgende mogelijke IPN-specificatie op basis van een identieke onderdeelbeschrijving + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 07907095..43d8303d 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -1859,6 +1859,18 @@ Po usunięciu pod elementy zostaną przeniesione na górę. Propozycje z numerycznymi przyrostami części + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktualna specyfikacja IPN dla części + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Następna możliwa specyfikacja IPN na podstawie identycznego opisu części + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 4d5e2d34..33f6b44b 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -1862,6 +1862,18 @@ Предложения с числовыми приращениями частей + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Текущая спецификация IPN для детали + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Следующая возможная спецификация IPN на основе идентичного описания детали + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index fb178f85..e8369214 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -1862,6 +1862,18 @@ 包含部件数值增量的建议 + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + 部件的当前IPN规格 + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + 基于相同部件描述的下一个可能的IPN规格 + + part.edit.tab.advanced.ipn.prefix_empty.direct_category