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,