product.features: Why-choose cards with icons (contract 0036-FE)

- new inline table tx_vitec_product_feature (title, cardtext, FAL icon)
  + `features` field on the product record; ProductShowJsonRenderer
  emits product.features {heading, items[{title, text, icon}]} and null
  while a product has no cards, so the front end keeps its
  applications-HTML fallback
- new vitec:seed-product-features: seeds the cards from the
  applications <p><strong> pairs and matches the staged agency icons by
  normalised title (camelCase split, stop words, Levenshtein <= 3 typo
  tolerance); products with existing cards are never touched
- new vitec:build-why-vitec-bands: "Why VITEC?" intro + icon_card band
  on the 7 overview pages with a staged icon set (the contract's
  "centered" variant does not exist - documented in ticket 0039-BE);
  vision/visionav sub-group sets stay unassigned
- product-check page: sortable title column and an Open Product button
- new migration fix_colombia_quote.php: stray quote element #12037
  removed, quote_role set (ticket 0038-FE)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5 committed 2026-11-04 19:55:01 +01:00
1 parent 3ccb9b149e
commit 46b12d4a58
8 files changed
+799 -4

No files matched your search

@@ -258,6 +258,11 @@ class ProductShowJsonRenderer
'resources' => $this->resourcesBand($uid, (string)(($product['resources_layout'] ?? '') ?: '50-50')),
];
// "Why choose <product>?" cards (contract 0036-FE): null while the
// product has no feature records - the front end then keeps parsing
// the `applications` HTML above.
$data['features'] = $this->featuresBand($uid, (string)$data['applications'], (string)$data['title']);
// schema.org JSON-LD (Product + optional VideoObject) as a ready-to-emit
// @graph string. The frontend injects it as <script type="application/ld+json">.
// Supersedes the manual `structureddata` field above (kept for backwards
@@ -267,6 +272,67 @@ class ProductShowJsonRenderer
return $data;
}
/**
* The "Why choose" card band: editor-maintained tx_vitec_product_feature
* records in sorting order, icons resolved to urls. The heading is the
* first <h3> of the applications HTML, falling back to the generic
* "Why choose <title>?".
*
* @return array{heading:string,items:array<int,array<string,mixed>>}|null
*/
protected function featuresBand(int $productUid, string $applicationsHtml, string $productTitle): ?array
{
try {
$pool = GeneralUtility::makeInstance(ConnectionPool::class);
$rows = $pool->getConnectionForTable('tx_vitec_product_feature')->fetchAllAssociative(
'SELECT uid, title, cardtext FROM tx_vitec_product_feature
WHERE deleted = 0 AND hidden = 0 AND product = ? ORDER BY sorting, uid',
[$productUid]
);
if ($rows === []) {
return null;
}
$icons = [];
foreach ($pool->getConnectionForTable('sys_file_reference')->fetchAllAssociative(
"SELECT r.uid_foreign, r.alternative, f.identifier FROM sys_file_reference r
JOIN sys_file f ON f.uid = r.uid_local
WHERE r.deleted = 0 AND r.hidden = 0 AND r.tablenames = 'tx_vitec_product_feature'
AND r.fieldname = 'icon'
AND r.uid_foreign IN (" . implode(',', array_map('intval', array_column($rows, 'uid'))) . ')
ORDER BY r.uid_foreign, r.sorting_foreign'
) as $iconRow) {
$icons[(int)$iconRow['uid_foreign']] ??= [
'url' => '/fileadmin' . (string)$iconRow['identifier'],
'alternative' => trim((string)($iconRow['alternative'] ?? '')),
];
}
$heading = '';
if (preg_match('#<h3[^>]*>(.*?)</h3>#is', $applicationsHtml, $m)) {
$heading = trim(strip_tags($m[1]));
}
if ($heading === '') {
$heading = 'Why choose ' . $productTitle . '?';
}
$items = [];
foreach ($rows as $row) {
$icon = $icons[(int)$row['uid']] ?? null;
if ($icon !== null && $icon['alternative'] === '') {
$icon['alternative'] = (string)$row['title'];
}
$items[] = [
'title' => (string)$row['title'],
'text' => trim((string)($row['cardtext'] ?? '')),
'icon' => $icon,
];
}
return ['heading' => $heading, 'items' => $items];
} catch (\Throwable) {
return null;
}
}
/**
* Build the product's structured-data @graph from the already-serialized
* product array. Exception-safe: returns '' on any failure.