product-check: Features column with icon coverage

- new "Features" column: check with cards-and-icons count per product
  (e.g. "5·4i"), header carries the filled total like the other columns
- detail pane lists every feature card with icon thumbnail (click to
  enlarge), title and text; cards without an icon show a red cross on
  the icon slot - the missing agency icon sets are visible at a glance

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5 committed 2026-11-04 20:17:50 +01:00
1 parent 6ff9c3e7a8
commit f525ce3c27
1 file changed
+66 -1
+66 -1
View File
@@ -89,6 +89,25 @@ foreach ($pdo->query(
$solutions[(int)$r['product']][] = (string)$r['title']; $solutions[(int)$r['product']][] = (string)$r['title'];
} }
// inline feature cards (Why choose ...?) + their icon references
$features = [];
foreach ($pdo->query(
'SELECT uid, product, title, cardtext FROM tx_vitec_product_feature
WHERE deleted = 0 AND hidden = 0 ORDER BY product, sorting, uid'
)->fetchAll() as $r) {
$features[(int)$r['product']][] = $r;
}
$featureIcons = [];
foreach ($pdo->query(
"SELECT r.uid_foreign, f.identifier, f.name
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'
ORDER BY r.uid_foreign, r.sorting_foreign"
)->fetchAll() as $r) {
$featureIcons[(int)$r['uid_foreign']] ??= ['url' => '/fileadmin' . $r['identifier'], 'name' => (string)$r['name']];
}
// ---- helpers ------------------------------------------------------------------ // ---- helpers ------------------------------------------------------------------
function h(string $v): string function h(string $v): string
@@ -133,6 +152,36 @@ function imageBlock(string $label, array $images): string
return $out . '</div></div>'; return $out . '</div></div>';
} }
/**
* Feature cards with their icon thumbnails (click to enlarge).
*
* @param array<int,array<string,mixed>> $items
* @param array<int,array{url:string,name:string}> $featureIcons
*/
function featuresBlock(array $items, array $featureIcons): string
{
if ($items === []) {
return '';
}
$withIcon = 0;
$cards = '';
foreach ($items as $it) {
$icon = $featureIcons[(int)$it['uid']] ?? null;
if ($icon !== null) {
$withIcon++;
}
$cards .= '<div class="feature">'
. ($icon !== null
? '<a href="' . h($icon['url']) . '" class="zoom ficon" title="' . h($icon['name']) . '">'
. '<img loading="lazy" src="' . h($icon['url']) . '" alt="' . h($icon['name']) . '"></a>'
: '<span class="ficon noicon" title="no icon">✗</span>')
. '<div><div class="ftitle">' . h((string)$it['title']) . '</div>'
. '<div class="ftext">' . h((string)$it['cardtext']) . '</div></div></div>';
}
return '<div class="fld"><div class="fldlabel">Features (' . count($items) . ' cards, ' . $withIcon . ' icons)</div>'
. '<div class="features">' . $cards . '</div></div>';
}
/** @param string[] $items */ /** @param string[] $items */
function listBlock(string $label, array $items): string function listBlock(string $label, array $items): string
{ {
@@ -145,7 +194,7 @@ function listBlock(string $label, array $items): string
// ---- columns ------------------------------------------------------------------ // ---- columns ------------------------------------------------------------------
$columns = ['Hero', 'Photos', 'Video', 'Teaser', 'Subtitle', 'Description', 'Descr. 2', 'Highlights', 'Capabilities', 'Applications', 'Resources', 'Datasheets', 'Solutions']; $columns = ['Hero', 'Photos', 'Video', 'Teaser', 'Subtitle', 'Description', 'Descr. 2', 'Highlights', 'Capabilities', 'Applications', 'Features', 'Resources', 'Datasheets', 'Solutions'];
$filledPerColumn = array_fill_keys($columns, 0); $filledPerColumn = array_fill_keys($columns, 0);
$rowsHtml = ''; $rowsHtml = '';
@@ -155,6 +204,8 @@ foreach ($products as $p) {
$res = $resources[$uid] ?? []; $res = $resources[$uid] ?? [];
$ds = $datasheets[$uid] ?? []; $ds = $datasheets[$uid] ?? [];
$sol = $solutions[$uid] ?? []; $sol = $solutions[$uid] ?? [];
$feat = $features[$uid] ?? [];
$featIconCount = count(array_filter($feat, static fn(array $it): bool => isset($featureIcons[(int)$it['uid']])));
$checks = [ $checks = [
'Hero' => [count($f['heroimage'] ?? []), true], 'Hero' => [count($f['heroimage'] ?? []), true],
@@ -167,6 +218,7 @@ foreach ($products as $p) {
'Highlights' => [filled($p['highlights']) ? 1 : 0, false], 'Highlights' => [filled($p['highlights']) ? 1 : 0, false],
'Capabilities' => [filled($p['capabilities']) ? 1 : 0, false], 'Capabilities' => [filled($p['capabilities']) ? 1 : 0, false],
'Applications' => [filled($p['applications']) ? 1 : 0, false], 'Applications' => [filled($p['applications']) ? 1 : 0, false],
'Features' => [count($feat), true],
'Resources' => [count($res), true], 'Resources' => [count($res), true],
'Datasheets' => [count($ds), true], 'Datasheets' => [count($ds), true],
'Solutions' => [count($sol), true], 'Solutions' => [count($sol), true],
@@ -176,6 +228,11 @@ foreach ($products as $p) {
foreach ($columns as $column) { foreach ($columns as $column) {
[$count, $showCount] = $checks[$column]; [$count, $showCount] = $checks[$column];
$filledPerColumn[$column] += $count > 0 ? 1 : 0; $filledPerColumn[$column] += $count > 0 ? 1 : 0;
if ($column === 'Features' && $count > 0) {
// cards + how many of them carry an icon
$cells .= '<td class="ok">✓<small>' . $count . '·' . $featIconCount . 'i</small></td>';
continue;
}
$cells .= cell($count > 0, $showCount ? $count : 0); $cells .= cell($count > 0, $showCount ? $count : 0);
} }
@@ -216,6 +273,7 @@ foreach ($products as $p) {
. textBlock('Highlights', (string)$p['highlights']) . textBlock('Highlights', (string)$p['highlights'])
. textBlock('Capabilities', (string)$p['capabilities']) . textBlock('Capabilities', (string)$p['capabilities'])
. textBlock('Applications', (string)$p['applications']) . textBlock('Applications', (string)$p['applications'])
. featuresBlock($feat, $featureIcons)
. listBlock('Resources', $resourceLines) . listBlock('Resources', $resourceLines)
. listBlock('Datasheets', $ds) . listBlock('Datasheets', $ds)
. listBlock('Solutions', $sol); . listBlock('Solutions', $sol);
@@ -280,6 +338,13 @@ tr.detail>td{background:#fafbfd;text-align:left;white-space:normal;padding:0}
.fldval{background:#fff;border:1px solid var(--line);border-radius:6px;padding:8px 12px;overflow-wrap:anywhere} .fldval{background:#fff;border:1px solid var(--line);border-radius:6px;padding:8px 12px;overflow-wrap:anywhere}
.fldval p:first-child{margin-top:0}.fldval p:last-child{margin-bottom:0} .fldval p:first-child{margin-top:0}.fldval p:last-child{margin-bottom:0}
.fldlist{margin:0;background:#fff;border:1px solid var(--line);border-radius:6px;padding:8px 12px 8px 28px} .fldlist{margin:0;background:#fff;border:1px solid var(--line);border-radius:6px;padding:8px 12px 8px 28px}
.features{display:grid;gap:8px}
.feature{display:flex;gap:10px;align-items:flex-start;background:#fff;border:1px solid var(--line);border-radius:6px;padding:8px 12px}
.ficon{width:40px;height:40px;flex:0 0 40px;display:flex;align-items:center;justify-content:center;border:1px solid var(--line);border-radius:6px;background:#fff}
.ficon img{max-width:30px;max-height:30px}
.noicon{color:var(--miss);background:#fdf0ee;font-weight:700}
.ftitle{font-weight:600}
.ftext{color:#555;font-size:12.5px}
.thumbs{display:flex;flex-wrap:wrap;gap:8px} .thumbs{display:flex;flex-wrap:wrap;gap:8px}
.thumbs a{display:flex;width:140px;height:92px;align-items:center;justify-content:center;background:#fff;border:1px solid var(--line);border-radius:6px;overflow:hidden} .thumbs a{display:flex;width:140px;height:92px;align-items:center;justify-content:center;background:#fff;border:1px solid var(--line);border-radius:6px;overflow:hidden}
.thumbs img{max-width:100%;max-height:100%;object-fit:contain} .thumbs img{max-width:100%;max-height:100%;object-fit:contain}