Content checks: flag placeholder images instead of counting them as done

Files below fileadmin/placeholders/ (or named "...placeholder...") are grey
dummies. PageCheckService and ProductCheckService now report them:

- image cells carry `placeholder` = number of placeholder files (only when > 0);
  `ok` is unchanged (something is attached)
- summary.filled counts only cells without placeholders, new summary.placeholder
  counts the cells that have one
- detail images carry `placeholder: true|false`
- cards: `placeholder` = cards whose icon/image is a placeholder; record: the
  record image

Additive change to the JSON contract; frontend rendering follows (ticket).
Requested by Harry 2026-11-13.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5.1 committed 2026-11-13 18:47:12 +01:00
1 parent 20bcbeb731
commit 17b08df283
3 files changed
+137 -47

No files matched your search

@@ -24,6 +24,13 @@ use TYPO3\CMS\Extbase\Service\ImageService;
* detail(): one product with all texts (RTE HTML as stored), images
* (original URL + ~480 px thumbnail), videos, feature cards
* with icons, resources, datasheets, solutions.
*
* Placeholders (Harry 2026-11-13, same rule as PageCheckService): files below
* fileadmin/placeholders/ or named "...placeholder..." do not pass as
* finished. Image cells carry `placeholder` = number of such files (only when
* > 0), summary.filled counts only cells without placeholders and
* summary.placeholder the cells that have one; detail images carry
* `placeholder: true|false`.
*/
final class ProductCheckService
{
@@ -84,20 +91,23 @@ final class ProductCheckService
$products = $this->products();
$uids = array_map(static fn(array $p): int => (int)$p['uid'], $products);
$files = $this->fileCounts($uids);
$placeholders = $this->fileCounts($uids, true);
$features = $this->featureCounts($uids);
$resources = $this->countBy('tx_vitec_product_resource', 'product', 'deleted = 0 AND hidden = 0');
$datasheets = $this->mmCounts('tx_vitec_product_datasheet_mm', 'tx_vitec_domain_model_download');
$solutions = $this->mmCounts('tx_vitec_product_solution_mm', 'tx_vitec_domain_model_solution');
$filled = array_fill_keys(array_column(self::FIELDS, 'key'), 0);
$placeholder = array_fill_keys(array_column(self::FIELDS, 'key'), 0);
$rows = [];
foreach ($products as $p) {
$uid = (int)$p['uid'];
$f = $files[$uid] ?? [];
$ph = $placeholders[$uid] ?? [];
$checks = [
'hero' => $this->check($f['heroimage'] ?? 0),
'photos' => $this->check($f['productimage'] ?? 0),
'og' => $this->check($f['ogimage'] ?? 0),
'hero' => $this->check($f['heroimage'] ?? 0, $ph['heroimage'] ?? 0),
'photos' => $this->check($f['productimage'] ?? 0, $ph['productimage'] ?? 0),
'og' => $this->check($f['ogimage'] ?? 0, $ph['ogimage'] ?? 0),
'video' => $this->check($f['videofile'] ?? 0),
'teaser' => $this->check($this->filled($p['teaser']) ? 1 : 0),
'subtitle' => $this->check($this->filled($p['subtitle']) ? 1 : 0),
@@ -112,7 +122,9 @@ final class ProductCheckService
'solutions' => $this->check($solutions[$uid] ?? 0),
];
foreach ($checks as $key => $c) {
if ($c['ok']) {
if (($c['placeholder'] ?? 0) > 0) {
$placeholder[$key]++;
} elseif ($c['ok']) {
$filled[$key]++;
}
}
@@ -122,7 +134,7 @@ final class ProductCheckService
return [
'generatedAt' => date(\DATE_ATOM),
'fields' => self::FIELDS,
'summary' => ['products' => count($rows), 'filled' => $filled],
'summary' => ['products' => count($rows), 'filled' => $filled, 'placeholder' => $placeholder],
'products' => $rows,
];
}
@@ -191,9 +203,9 @@ final class ProductCheckService
}
/** @return array{ok: bool, count: int} */
private function check(int $count): array
private function check(int $count, int $placeholder = 0): array
{
return ['ok' => $count > 0, 'count' => $count];
return ['ok' => $count > 0, 'count' => $count] + ($placeholder > 0 ? ['placeholder' => $placeholder] : []);
}
private function filled(mixed $value): bool
@@ -218,18 +230,24 @@ final class ProductCheckService
* @param int[] $uids
* @return array<int, array<string, int>> [productUid][fieldname] = count
*/
private function fileCounts(array $uids): array
private function fileCounts(array $uids, bool $placeholdersOnly = false): array
{
if ($uids === []) {
return [];
}
$rows = $this->connection()->executeQuery(
"SELECT uid_foreign, fieldname, COUNT(*) AS n FROM sys_file_reference
WHERE deleted = 0 AND hidden = 0 AND tablenames = 'tx_vitec_domain_model_product'
AND fieldname IN ('heroimage','productimage','ogimage','videofile')
AND uid_foreign IN (" . implode(',', array_map('intval', $uids)) . ')
GROUP BY uid_foreign, fieldname'
)->fetchAllAssociative();
$in = implode(',', array_map('intval', $uids));
$sql = $placeholdersOnly
? "SELECT r.uid_foreign, r.fieldname, COUNT(*) AS n 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_domain_model_product'
AND r.fieldname IN ('heroimage','productimage','ogimage','videofile')
AND r.uid_foreign IN (" . $in . ') AND ' . PageCheckService::PLACEHOLDER_SQL . '
GROUP BY r.uid_foreign, r.fieldname'
: "SELECT uid_foreign, fieldname, COUNT(*) AS n FROM sys_file_reference
WHERE deleted = 0 AND hidden = 0 AND tablenames = 'tx_vitec_domain_model_product'
AND fieldname IN ('heroimage','productimage','ogimage','videofile')
AND uid_foreign IN (" . $in . ')
GROUP BY uid_foreign, fieldname';
$rows = $this->connection()->executeQuery($sql)->fetchAllAssociative();
$out = [];
foreach ($rows as $r) {
$out[(int)$r['uid_foreign']][(string)$r['fieldname']] = (int)$r['n'];
@@ -241,7 +259,7 @@ final class ProductCheckService
* Files of one product with original URL and a ~480 px thumbnail
* (SVG and non-images keep the original as thumbnail).
*
* @return array<string, array<int, array{url: string, thumb: string, name: string, mime: string}>>
* @return array<string, array<int, array{url: string, thumb: string, name: string, mime: string, placeholder: bool}>>
*/
private function files(int $uid): array
{
@@ -261,6 +279,7 @@ final class ProductCheckService
'thumb' => $this->thumbnail((int)$r['uid_local'], (string)$r['mime_type'], $url),
'name' => (string)$r['name'],
'mime' => (string)$r['mime_type'],
'placeholder' => PageCheckService::isPlaceholder((string)$r['identifier'], (string)$r['name']),
];
}
return $out;