From 681146334352e28a08634f179988780b885fb10c Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Thu, 24 Sep 2026 14:47:04 +0200 Subject: [PATCH] Product texts: mapping completed; product model: solutions + resources Two work packages, one deploy. Workbook field mapping completed (2026-09-18) - highlights <- g:Technical Highlight (new spec-row component of the full agency delivery; composeGroup() is generic, no change needed) - applications <- g:Why Choose Card (benefit cards as one block) - seometa <- new composed source x:seometa (Introduction body trimmed at a word boundary to ~155 chars - the SEO sheet ships no meta description, a trimmed intro beats an empty tag) - teaser stays the hero one-liner, settling the varchar(255) question - fix: "Save mapping" now merges over the stored mapping instead of overwriting it - an empty dropdown no longer wipes a stored selector Product model: solutions relation and Resources tab (2026-09-24) - closes the gaps the full workbook walkthrough exposed (rows the import deliberately skips because the model had no place for them) - solutions: MM multi select to tx_vitec_domain_model_solution (tx_vitec_product_solution_mm, relatedprodukt pattern), emitted by both product renderers as solution cards in the shared shape - Resources tab: inline child table tx_vitec_product_resource - per entry a type (documentation/support), an editable card title/text, an icon (free text with a value-picker seed set) and one link (page/url/file/record via the link wizard). Deliberately no Extbase model: only the UserFunc path reads the rows. - datasheets: own MM selection of download records below the resource list (tx_vitec_product_datasheet_mm); emitted into the SAME resources envelope as ready-made type=datasheet cards (title/teaser/ icon from the record, url = the public /download/ route, uid fallback - private/hidden downloads 404 as everywhere) - JSON `resources` (detail payload only, like heroimage): list envelope {layout, items[]}; a hand-written card whose link is empty or unresolvable is not emitted; layout switches to carousel above three cards in total - solutions hybrid (same day, after analysing all 100 workbooks: their Solution Cards reference the 8 GROUPS in 98% of cases): new is_group flag on the solution model, new idempotent CLI vitec:create-solution-groups (8 group records; title source is AlignSolutionsCommand::TARGET - now public -, category = the group's own category, detail_page = the /solutions group page with -N disambiguation). Group records are invisible to the solution list and to vitec:align-solutions and only selectable in the product relation; product solution cards carry isGroup - spec raised to v1.18 (7.15 member table, 7.13.3 note, revision row; the footer had been stuck at v1.15 since 1.16/1.17) Requires on the server: vendor/bin/typo3 database:updateschema "*.add,*.change" vendor/bin/typo3 cache:flush Co-Authored-By: Claude Fable 5 --- .../Classes/Command/AlignSolutionsCommand.php | 6 +- .../Command/CreateSolutionGroupsCommand.php | 193 ++++++++++++++++++ .../vitec/Classes/Domain/Model/Product.php | 24 +++ .../vitec/Classes/Domain/Model/Solution.php | 18 ++ .../UserFunc/ProductListJsonRenderer.php | 1 + .../UserFunc/ProductShowJsonRenderer.php | 32 +++ .../UserFunc/SolutionListJsonRenderer.php | 5 +- .../TCA/tx_vitec_domain_model_product.php | 17 +- .../TCA/tx_vitec_domain_model_solution.php | 12 +- .../TCA/tx_vitec_product_resource.php | 8 +- .../Headless-JSON-Architecture.md | 12 +- packages/vitec/ext_tables.sql | 9 + 12 files changed, 325 insertions(+), 12 deletions(-) create mode 100644 packages/vitec/Classes/Command/CreateSolutionGroupsCommand.php diff --git a/packages/vitec/Classes/Command/AlignSolutionsCommand.php b/packages/vitec/Classes/Command/AlignSolutionsCommand.php index 2fa2f56..a325006 100644 --- a/packages/vitec/Classes/Command/AlignSolutionsCommand.php +++ b/packages/vitec/Classes/Command/AlignSolutionsCommand.php @@ -49,7 +49,7 @@ final class AlignSolutionsCommand extends Command private const TABLE = 'tx_vitec_domain_model_solution'; /** Binding target: group => solutions, verbatim from the sheet (typo fixed). */ - private const TARGET = [ + public const TARGET = [ 'Control Room & Command Centre Solutions' => [ 'Control Room Platforms (SOC / NOC / Command Centres)', 'Command & Control Visualisation', @@ -170,8 +170,10 @@ final class AlignSolutionsCommand extends Command } $solutions = []; + // is_group records (the 8 group-level cards, hybrid 2026-09-24) are + // invisible to this command: they are neither targets nor prunable. foreach ($connection->fetchAllAssociative( - 'SELECT uid, title, slug FROM ' . self::TABLE . ' WHERE deleted = 0' + 'SELECT uid, title, slug FROM ' . self::TABLE . ' WHERE deleted = 0 AND is_group = 0' ) as $row) { $solutions[(string)$row['title']] = ['uid' => (int)$row['uid'], 'slug' => (string)$row['slug']]; } diff --git a/packages/vitec/Classes/Command/CreateSolutionGroupsCommand.php b/packages/vitec/Classes/Command/CreateSolutionGroupsCommand.php new file mode 100644 index 0000000..5e63ebf --- /dev/null +++ b/packages/vitec/Classes/Command/CreateSolutionGroupsCommand.php @@ -0,0 +1,193 @@ +addOption('dry-run', null, InputOption::VALUE_NONE, 'Report only - nothing written'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + Bootstrap::initializeBackendAuthentication(); + $dryRun = (bool)$input->getOption('dry-run'); + $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::TABLE); + + $groups = array_keys(AlignSolutionsCommand::TARGET); + + // Existing records: titles for idempotency, slugs for collision checks. + $existingByTitle = []; + $usedSlugs = []; + foreach ($connection->fetchAllAssociative( + 'SELECT uid, title, slug, is_group FROM ' . self::TABLE . ' WHERE deleted = 0' + ) as $row) { + $existingByTitle[$this->normalize((string)$row['title'])] = $row; + $usedSlugs[(string)$row['slug']] = true; + } + + $pids = $connection->fetchFirstColumn('SELECT pid FROM ' . self::TABLE . ' WHERE deleted = 0'); + $pidCounts = array_count_values(array_map('intval', $pids)); + arsort($pidCounts); + $storagePid = (int)(array_key_first($pidCounts) ?? 0); + if ($storagePid <= 0) { + $output->writeln('No storage pid detectable (no solution records?)'); + return Command::FAILURE; + } + + // Group categories below the root category "Solution". + $rootCat = (int)($connection->fetchOne( + "SELECT uid FROM sys_category WHERE deleted = 0 AND parent = 0 AND title = 'Solution'" + ) ?: 0); + $catByTitle = []; + if ($rootCat > 0) { + foreach ($connection->fetchAllAssociative( + 'SELECT uid, title FROM sys_category WHERE deleted = 0 AND parent = ?', + [$rootCat] + ) as $row) { + $catByTitle[$this->normalize((string)$row['title'])] = (int)$row['uid']; + } + } + + // Candidate pages: standard pages, hidden allowed (map-solution-pages rule). + $pagesByTitle = []; + foreach ($connection->fetchAllAssociative( + 'SELECT uid, title, slug FROM pages WHERE deleted = 0 AND doktype = 1' + ) as $row) { + $pagesByTitle[$this->normalize((string)$row['title'])][] = $row; + } + + $datamap = []; + $newIndex = 0; + $created = 0; + foreach ($groups as $group) { + $key = $this->normalize($group); + $existing = $existingByTitle[$key] ?? null; + if ($existing !== null) { + $state = ((int)$existing['is_group'] === 1) ? 'ok (is_group=1)' : 'KONFLIKT: Record gleichen Titels ohne is_group'; + $output->writeln(sprintf(' %-45s existiert, uid %d - %s', $group, (int)$existing['uid'], $state)); + continue; + } + + $pageUid = 0; + $candidates = $pagesByTitle[$key] ?? []; + if (count($candidates) > 1) { + // `-N` duplicate slugs lose the disambiguation. + $candidates = array_values(array_filter( + $candidates, + static fn(array $p): bool => !preg_match('/-\d+$/', (string)$p['slug']) + )) ?: $candidates; + } + if (count($candidates) === 1) { + $pageUid = (int)$candidates[0]['uid']; + } elseif (count($candidates) > 1) { + $output->writeln(sprintf(' %s: Seite mehrdeutig (%d Kandidaten) - detail_page bleibt leer', $group, count($candidates))); + } else { + $output->writeln(sprintf(' %s: keine Gruppen-Seite gefunden - detail_page bleibt leer', $group)); + } + + $slug = $this->slugFor($group, $usedSlugs); + $fields = [ + 'pid' => $storagePid, + 'title' => $group, + 'slug' => $slug, + 'is_group' => 1, + 'detail_page' => $pageUid, + ]; + $catUid = $catByTitle[$key] ?? 0; + if ($catUid > 0) { + $fields['categories'] = (string)$catUid; + } else { + $output->writeln(sprintf(' %s: keine Gruppen-Kategorie gefunden', $group)); + } + + $output->writeln(sprintf( + ' %-45s %s slug=%s page=%s cat=%s', + $group, + $dryRun ? 'wuerde angelegt' : 'anlegen', + $slug, + $pageUid ?: '-', + $catUid ?: '-' + )); + $datamap[self::TABLE]['NEWGROUP' . (++$newIndex)] = $fields; + $created++; + } + + if ($dryRun) { + $output->writeln(sprintf('DRY-RUN: %d Gruppen-Records wuerden angelegt.', $created)); + return Command::SUCCESS; + } + + if ($datamap !== []) { + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->start($datamap, []); + $dataHandler->process_datamap(); + foreach ($dataHandler->errorLog as $error) { + $output->writeln("$error"); + } + } + $output->writeln(sprintf('%d Gruppen-Records angelegt.', $created)); + + return Command::SUCCESS; + } + + private function normalize(string $title): string + { + $title = str_replace('&', 'and', mb_strtolower($title)); + $title = (string)preg_replace('/[^a-z0-9]+/', ' ', $title); + return trim((string)preg_replace('/\s+/', ' ', $title)); + } + + /** @param array $usedSlugs */ + private function slugFor(string $title, array &$usedSlugs): string + { + $slug = trim((string)preg_replace('/[^a-z0-9]+/', '-', str_replace('&', 'and', mb_strtolower($title))), '-'); + $candidate = $slug; + $i = 1; + while (isset($usedSlugs[$candidate])) { + $candidate = $slug . '-group' . ($i > 1 ? '-' . $i : ''); + $i++; + } + $usedSlugs[$candidate] = true; + return $candidate; + } +} diff --git a/packages/vitec/Classes/Domain/Model/Product.php b/packages/vitec/Classes/Domain/Model/Product.php index 4e18866..67a3d1c 100755 --- a/packages/vitec/Classes/Domain/Model/Product.php +++ b/packages/vitec/Classes/Domain/Model/Product.php @@ -723,6 +723,7 @@ class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $this->heroimage = new ObjectStorage(); $this->downloads = new ObjectStorage(); $this->solutions = new ObjectStorage(); + $this->datasheets = new ObjectStorage(); } /** @@ -1097,6 +1098,29 @@ class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $this->solutions = $solutions; } + /** + * Datasheet downloads for the Resources tab (MM) + * + * @var ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> + */ + protected $datasheets; + + /** + * @return ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> + */ + public function getDatasheets(): ObjectStorage + { + return $this->datasheets; + } + + /** + * @param ObjectStorage<\Evomedien\Vitec\Domain\Model\Download> $datasheets + */ + public function setDatasheets(ObjectStorage $datasheets): void + { + $this->datasheets = $datasheets; + } + /* --------------------------------------------------------------------- */ } diff --git a/packages/vitec/Classes/Domain/Model/Solution.php b/packages/vitec/Classes/Domain/Model/Solution.php index 02db470..c293010 100644 --- a/packages/vitec/Classes/Domain/Model/Solution.php +++ b/packages/vitec/Classes/Domain/Model/Solution.php @@ -204,4 +204,22 @@ class Solution extends AbstractEntity { $this->categories = $categories; } + + /** + * Group-level record (one of the 8 solution groups) - hybrid decision + * 2026-09-24. Excluded from lists; selectable in product relations. + * + * @var bool + */ + protected $isGroup = false; + + public function getIsGroup(): bool + { + return $this->isGroup; + } + + public function setIsGroup(bool $isGroup): void + { + $this->isGroup = $isGroup; + } } diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php index 24238a7..bb0aacf 100755 --- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php @@ -947,6 +947,7 @@ class ProductListJsonRenderer 'teaser' => (string)($solutionRow['teaser'] ?? ''), 'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''), 'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)), + 'isGroup' => (bool)($solutionRow['is_group'] ?? false), 'image' => $serializer->image($solutionUid, 'image', 'tx_vitec_domain_model_solution', true), ]; } diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php index d99a3f4..045368a 100755 --- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php @@ -771,6 +771,7 @@ class ProductShowJsonRenderer 'teaser' => (string)($solutionRow['teaser'] ?? ''), 'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''), 'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)), + 'isGroup' => (bool)($solutionRow['is_group'] ?? false), 'image' => $serializer->image($solutionUid, 'image', 'tx_vitec_domain_model_solution', true), ]; } @@ -817,6 +818,37 @@ class ProductShowJsonRenderer ]; } + // Datasheets are their own MM selection of download records and join + // the same envelope as ready-made cards: url is the public forced- + // download route (slug route since 2026-08-21, uid route as + // fallback), so the DownloadFileResolver rules apply unchanged - + // a private or hidden download simply 404s. + $datasheetQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class) + ->getQueryBuilderForTable('tx_vitec_domain_model_download'); + $datasheetRows = $datasheetQueryBuilder + ->select('d.uid', 'd.title', 'd.slug', 'd.teaser', 'd.icon') + ->from('tx_vitec_domain_model_download', 'd') + ->join('d', 'tx_vitec_product_datasheet_mm', 'mm', 'mm.uid_foreign = d.uid') + ->where( + $datasheetQueryBuilder->expr()->eq('mm.uid_local', $datasheetQueryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)), + $datasheetQueryBuilder->expr()->eq('d.deleted', 0), + $datasheetQueryBuilder->expr()->eq('d.hidden', 0) + ) + ->orderBy('mm.sorting', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + + foreach ($datasheetRows as $datasheetRow) { + $slug = trim((string)($datasheetRow['slug'] ?? ''), "/ "); + $items[] = [ + 'type' => 'datasheet', + 'title' => (string)($datasheetRow['title'] ?? ''), + 'text' => (string)($datasheetRow['teaser'] ?? ''), + 'icon' => ((string)($datasheetRow['icon'] ?? '')) !== '' ? (string)$datasheetRow['icon'] : 'datasheet', + 'url' => $slug !== '' ? '/download/' . $slug : '/download/file/' . (int)$datasheetRow['uid'], + ]; + } + return [ 'layout' => count($items) > 3 ? 'carousel' : 'grid', 'items' => $items, diff --git a/packages/vitec/Classes/UserFunc/SolutionListJsonRenderer.php b/packages/vitec/Classes/UserFunc/SolutionListJsonRenderer.php index 523c2e8..5311bda 100644 --- a/packages/vitec/Classes/UserFunc/SolutionListJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/SolutionListJsonRenderer.php @@ -126,7 +126,10 @@ class SolutionListJsonRenderer ->from(self::TABLE) ->where( $qb->expr()->eq('deleted', 0), - $qb->expr()->eq('hidden', 0) + $qb->expr()->eq('hidden', 0), + // Group-level records (hybrid 2026-09-24) exist for the + // product relation - never as a 40th+ list entry. + $qb->expr()->eq('is_group', 0) ) ->orderBy('title', 'ASC') ->executeQuery() diff --git a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php index c9eb9d1..9a32e89 100755 --- a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php +++ b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_product.php @@ -24,7 +24,7 @@ return [ ], 'types' => [ '1' => ['showitem' => 'title, subtitle, slug, teaser, description, description2, applications, capabilities, highlights, contentelement, contentelementcta, downloads, portfolio, showdatapath, textrelatedproducts, relatedprodukt, solutions, - --div--;Resources, resources, + --div--;Resources, resources, datasheets, --div--;SEO, seotitle, urltitle, seometa, keywords, structureddata, --div--;Images and Videos, heroimage, productimage, ogimage, video, videofile, --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories, @@ -463,6 +463,21 @@ return [ ], ], ], + 'datasheets' => [ + 'exclude' => true, + 'label' => 'Datasheets', + 'description' => 'Download records shown as datasheet cards below the resource cards - same card row, counts towards the carousel rule', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectMultipleSideBySide', + 'foreign_table' => 'tx_vitec_domain_model_download', + 'MM' => 'tx_vitec_product_datasheet_mm', + 'size' => 8, + 'maxitems' => 9999, + 'minitems' => 0, + 'enableMultiSelectFilterTextfield' => true, + ], + ], 'productimage' => [ 'exclude' => true, 'label' => 'Product Image(s)', diff --git a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_solution.php b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_solution.php index 19d2cb9..044e039 100644 --- a/packages/vitec/Configuration/TCA/tx_vitec_domain_model_solution.php +++ b/packages/vitec/Configuration/TCA/tx_vitec_domain_model_solution.php @@ -24,7 +24,7 @@ return [ ], 'types' => [ '1' => [ - 'showitem' => 'title, slug, detail_page, subtitle, teaser, description, image, + 'showitem' => 'title, slug, detail_page, is_group, subtitle, teaser, description, image, --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories, --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, sys_language_uid, l10n_parent, l10n_diffsource, --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, hidden, starttime, endtime', @@ -140,6 +140,16 @@ return [ 'maxitems' => 1, ], ], + 'is_group' => [ + 'exclude' => true, + 'label' => 'Is Group', + 'description' => 'Group-level record (one of the 8 solution groups, hybrid decision 2026-09-24). Kept out of solution lists and vitec:align-solutions; selectable in the product "Related Solutions" field.', + 'config' => [ + 'type' => 'check', + 'renderType' => 'checkboxToggle', + 'default' => 0, + ], + ], 'subtitle' => [ 'exclude' => true, 'label' => 'Subtitle', diff --git a/packages/vitec/Configuration/TCA/tx_vitec_product_resource.php b/packages/vitec/Configuration/TCA/tx_vitec_product_resource.php index d9cc146..fa56384 100644 --- a/packages/vitec/Configuration/TCA/tx_vitec_product_resource.php +++ b/packages/vitec/Configuration/TCA/tx_vitec_product_resource.php @@ -2,8 +2,10 @@ /** * Resource cards on the product's Resources tab (decision 2026-09-24): - * documentation / support / datasheet entries, several per type, each an - * editable card (title, text, icon) around one link. Child table of + * documentation / support entries, several per type, each an editable card + * (title, text, icon) around one link. Datasheets are NOT an option here - + * they are the `datasheets` MM selection of download records next to this + * list and join the same JSON envelope as ready-made cards. Child table of * tx_vitec_domain_model_product via the `resources` inline field - edited * only there (hideTable), no Extbase model on purpose: the headless * renderers query the rows directly, the Extbase path never touches them. @@ -59,7 +61,6 @@ return [ 'items' => [ ['label' => 'Product Documentation', 'value' => 'documentation'], ['label' => 'Technical Support', 'value' => 'support'], - ['label' => 'Datasheet', 'value' => 'datasheet'], ], 'default' => 'documentation', ], @@ -97,7 +98,6 @@ return [ 'items' => [ ['label' => 'Document', 'value' => 'document'], ['label' => 'Support', 'value' => 'support'], - ['label' => 'Datasheet', 'value' => 'datasheet'], ], ], ], diff --git a/packages/vitec/Documentation/Headless-JSON-Architecture.md b/packages/vitec/Documentation/Headless-JSON-Architecture.md index 80c087c..b885fea 100755 --- a/packages/vitec/Documentation/Headless-JSON-Architecture.md +++ b/packages/vitec/Documentation/Headless-JSON-Architecture.md @@ -30,7 +30,7 @@ | 1.15 | 2026‑09‑11 | **Corpus change (additive, index side).** Download documents now carry the extracted file text: Solr 10 has no ExtractingRequestHandler, so a dedicated Apache Tika container on the Solr VPS (Caddy route `/tika/*`, own basic-auth credential) extracts PDF/Office contents during indexing (`TikaDownloadContentIndexer` on `BeforeDocumentIsProcessedForIndexingEvent`, fail-soft per 9.5, cached in `var/tika-cache` keyed on path+size+mtime). Datasheet specifications are now searchable ("625i", "genlock"). No interface change. | | 1.16 | 2026‑09‑11 | **New plugin** `vitec_productfinder` (`ProductFinderJsonRenderer`, payload key `productfinder`): the product category hierarchy on its own, as the data source for the filter selects above the product lists (7.15.3, 8). No change to `vitec_productlist` — filtering happens client side over the products already delivered, keyed on the category uids this payload carries. | | 1.17 | 2026‑09‑11 | **Interface change (additive).** The product list payload gained `successStories` (7.15.2.1): stories an editor attaches to a `sys_category` (tab *Options*, with its own layout), emitted in the list envelope beside `products`. Each card carries `forCategories` — the product categories it came from — so the front end can hide it with the same `?cat=`/`?subcat=` filter it applies to the products. Story links reuse the 7.5 rule, with the detail page taken from the element or the new site setting `vitec.storyDetailPid`. | -| 1.18 | 2026‑09‑24 | **Interface change (additive).** Products gained `solutions` — an MM relation to solutions, emitted as solution cards in both product payloads — and a **Resources tab**: child table `tx_vitec_product_resource` with documentation / support / datasheet cards (editable title, text and icon around one link, resolved per 9.12; cards without a resolvable link are not emitted), delivered detail‑only as the `resources` list envelope whose `layout` switches to `carousel` above three cards (7.15). Editorial: the footer had been stuck at v1.15 since 1.16/1.17 (2026‑09‑11). | +| 1.18 | 2026‑09‑24 | **Interface change (additive).** Products gained `solutions` — an MM relation to solutions, emitted as solution cards in both product payloads — and a **Resources tab**: child table `tx_vitec_product_resource` with documentation / support cards (editable title, text and icon around one link, resolved per 9.12; cards without a resolvable link are not emitted) plus a `datasheets` MM selection of download records rendered into the same envelope as `type: datasheet` cards, delivered detail‑only as the `resources` list envelope whose `layout` switches to `carousel` above three cards in total (7.15). The `solutions` relation accepts group‑level records: the eight groups exist as `is_group = 1` solutions (created by `vitec:create-solution-groups`, excluded from lists and from `vitec:align-solutions`) because the agency workbooks reference the group level. Editorial: the footer had been stuck at v1.15 since 1.16/1.17 (2026‑09‑11). | This document is drafted in the style of, and adopts the terminology conventions of, ISO/IEC/IEEE 42010 (architecture description), ISO/IEC/IEEE 26514 (information for @@ -801,6 +801,12 @@ practice. Image resolution is delegated to `UsecaseSerializer::image()` per 9.2; renderer duplicates no FAL logic. #### 7.13.3 Solution list payload (`vitec_solutionlist`) + +> **NOTE (since v1.18)** Group‑level solution records (`is_group = 1`, the eight +> thematic groups) never appear in this list. They exist to be selected in the +> product `solutions` relation (7.15) and carry the group page as `detailUrl`; +> `vitec:align-solutions` ignores them, `vitec:create-solution-groups` creates +> them idempotently. Added in v1.9, and deliberately the same shape as 7.13.2 — `SolutionListJsonRenderer` emits under `content.solutions`: @@ -898,8 +904,8 @@ Produced by `ProductShowJsonRenderer::serializeProduct()`. Members: | `downloads` | array | download records incl. the resolved `file` | | `ogimage`, `videofile` | object \| null | FAL singletons | | `relatedprodukt` | array | card shape `{uid, title, slug, subtitle, teaser, description, link, images}`, backend order | -| `solutions` | array | solution card shape `{uid, title, slug, subtitle, teaser, description, detailUrl, image}` via `tx_vitec_product_solution_mm`, backend order | -| `resources` | object | list envelope `{layout, items[]}` of hand‑written resource cards: `type` (`documentation\|support\|datasheet`), `title`, `text`, `icon`, resolved `url` (9.12); a card without a resolvable link is dropped; `layout` is `carousel` above three cards — **detail payload only** | +| `solutions` | array | solution card shape `{uid, title, slug, subtitle, teaser, description, detailUrl, isGroup, image}` via `tx_vitec_product_solution_mm`, backend order; an editor may select individual Generic Solutions or the eight group‑level records (`isGroup: true`, linking to the group page) | +| `resources` | object | list envelope `{layout, items[]}`: hand‑written cards (`type` `documentation\|support`, `title`, `text`, `icon`, resolved `url` per 9.12; a card without a resolvable link is dropped) followed by the `datasheets` MM selection of download records as `type: datasheet` cards (title/teaser/icon from the record, `url` = the public download route); `layout` is `carousel` above three cards in total — **detail payload only** | | `jsonLd` | string | ready‑to‑emit schema.org `@graph` (6.6), detail only | #### 7.15.2 List payload (`vitec_productlist`, key `products`) diff --git a/packages/vitec/ext_tables.sql b/packages/vitec/ext_tables.sql index 1cfcf8b..605583c 100755 --- a/packages/vitec/ext_tables.sql +++ b/packages/vitec/ext_tables.sql @@ -60,6 +60,7 @@ CREATE TABLE tx_vitec_domain_model_product ( textrelatedproducts text, solutions int(11) DEFAULT '0' NOT NULL, resources int(11) DEFAULT '0' NOT NULL, + datasheets int(11) DEFAULT '0' NOT NULL, KEY parent (pid) ); CREATE TABLE tx_vitec_domain_model_download ( @@ -106,6 +107,13 @@ CREATE TABLE tx_vitec_product_solution_mm ( PRIMARY KEY (uid_local, uid_foreign) ); +CREATE TABLE tx_vitec_product_datasheet_mm ( + uid_local INT(11) NOT NULL, + uid_foreign INT(11) NOT NULL, + sorting INT(11) DEFAULT '0' NOT NULL, + PRIMARY KEY (uid_local, uid_foreign) +); + CREATE TABLE tx_vitec_product_resource ( uid int(11) NOT NULL auto_increment, pid int(11) DEFAULT '0' NOT NULL, @@ -190,6 +198,7 @@ CREATE TABLE tx_vitec_domain_model_solution ( title varchar(255) DEFAULT '' NOT NULL, slug varchar(255) DEFAULT '' NOT NULL, detail_page int(11) DEFAULT '0' NOT NULL, + is_group tinyint(4) DEFAULT '0' NOT NULL, subtitle varchar(255) DEFAULT '' NOT NULL, teaser varchar(255) DEFAULT '' NOT NULL, description text,