Products run: missing records, asset re-run, audit page, category pages

- new vitec:create-missing-products: MGW Diamond TOUGH, MGW Pico TOUGH,
  MGW Pico+ TOUGH and X-View Integrated Displays as visible records
  (teaser drafts, pid and sys_category assignments copied from the
  sibling product; titles equal the Dropbox folder names so the image
  importer matches immediately). IQS4 stays out - its delivery folder
  is empty, question to Joe
- asset re-run via vitec:import-product-images after re-staging the
  Dropbox delivery (the misfiled Diamond-OG banner corrected into the
  OG inbox folder again): new photos for 95-Series, Avedia chassis,
  SFF PRISM and the new TOUGH/X-View records
- new migrations/build_product_audit.php: generates the internal
  product-asset audit at /product-audit/ (same basic auth, same-origin
  thumbnails) - one row per record with thumbnail, detail link, badges
  for hero/photos/texts/resources/datasheets/solutions, flags, stats
  and client-side filters; re-run any time
- new vitec:import-product-category-pages: builds the nine
  /products/<category> pages (frontend layout 9, hero/main/preFooter)
  from the agency "Product Category Pages" workbooks - intro
  paragraphs, Why-VITEC / product / solution cards, the comparison
  rows as ONE text element with a plain table (no comparison CType
  exists; FE styles it), prefooter banner to /contact, full page
  properties, page sorting per sitemap 3.1-3.9; the empty shells #455
  and #457 are reused. Product card links resolve record-first (exact
  title, stripped parenthesis, family alias, unique slug prefix);
  ProductFamilyAliases gains ten category-level family labels. Solution
  cards tolerate US/UK spelling (center/centre, visualization/
  visualisation). Applied and verified live (nine routes, links,
  tables, meta)
- known gaps reported, queued for Joe: IQS4, Media Players (93/94),
  Chassis Systems, Express Backplanes, Accessories & Cables, PCIe
  Encoders, VMS Plugins & Integrations; the Vision family card awaits
  a flagship decision

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5 committed 2026-10-27 13:31:03 +01:00
1 parent 8497dad06a
commit e2d4367d7a
5 files changed
+877

No files matched your search

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Creates the four product records the agency delivery references but the
* database never had (decision Olli 2026-10-27; they were a sitemap-tracker
* question before, their asset folders arrived with the Dropbox delivery):
*
* MGW Diamond TOUGH, MGW Pico TOUGH, MGW Pico+ TOUGH,
* X-View Integrated Displays
*
* Each new record copies pid and sys_category assignments from a named
* SIBLING product (so catalog panel and product finder place it correctly)
* and is created VISIBLE (dev decision Olli 2026-10-27) with a short
* placeholder teaser (Claude draft, English) - replace it once the agency
* texts arrive. Titles equal
* the Dropbox folder names, so a vitec:import-product-images re-run attaches
* the delivered banner/carousel assets immediately.
*
* IQS4 stays out on purpose: its delivery folder is empty - question to Joe.
*
* vendor/bin/typo3 vitec:create-missing-products
* vendor/bin/typo3 vitec:create-missing-products --apply
*/
#[AsCommand(
name: 'vitec:create-missing-products',
description: 'Create the missing TOUGH / X-View product records (dry-run by default)'
)]
final class CreateMissingProductsCommand extends Command
{
private const TABLE = 'tx_vitec_domain_model_product';
/** title (= Dropbox folder name), slug, sibling slug (pid + categories donor), teaser draft */
private const PRODUCTS = [
['MGW Diamond TOUGH', 'mgw-diamond-tough', 'mgw-diamond-sdi-encoder',
'Rugged four-channel HEVC/H.264 encoder built for harsh and mobile operational environments.'],
['MGW Pico TOUGH', 'mgw-pico-tough', 'mgw-pico-portable-encoder',
'Rugged portable H.264 encoder for low-latency video contribution in the field.'],
['MGW Pico+ TOUGH', 'mgw-pico-plus-tough', 'mgw-pico-portable-encoder',
'Rugged portable HEVC/H.264 encoder for demanding field deployments.'],
['X-View Integrated Displays', 'x-view-integrated-displays', 'x-player-end-points-xp23-xp25-xp26',
'Commercial displays with an integrated IPTV end-point for TV and signage delivery.'],
];
protected function configure(): void
{
$this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
Bootstrap::initializeBackendAuthentication();
$apply = (bool)$input->getOption('apply');
$pool = GeneralUtility::makeInstance(ConnectionPool::class);
$conn = $pool->getConnectionForTable(self::TABLE);
$output->writeln(($apply ? 'APPLY' : 'DRY-RUN'));
$datamap = [];
$index = 0;
foreach (self::PRODUCTS as [$title, $slug, $siblingSlug, $teaser]) {
$existing = $conn->fetchAssociative(
'SELECT uid FROM ' . self::TABLE . ' WHERE deleted = 0 AND (slug = ? OR title = ?)', [$slug, $title]
);
if ($existing) {
$output->writeln(sprintf(' = "%s" exists (#%d) - skipped', $title, (int)$existing['uid']));
continue;
}
$sibling = $conn->fetchAssociative(
'SELECT uid, pid FROM ' . self::TABLE . ' WHERE deleted = 0 AND slug = ?', [$siblingSlug]
);
if (!$sibling) {
$output->writeln(sprintf(' <error>! sibling %s not found - "%s" skipped</error>', $siblingSlug, $title));
continue;
}
$categories = $pool->getConnectionForTable('sys_category_record_mm')->fetchFirstColumn(
"SELECT uid_local FROM sys_category_record_mm
WHERE tablenames = ? AND fieldname = 'categories' AND uid_foreign = ? ORDER BY sorting_foreign",
[self::TABLE, (int)$sibling['uid']]
);
$datamap[self::TABLE]['NEWPROD' . (++$index)] = [
'pid' => (int)$sibling['pid'],
'title' => $title,
'slug' => $slug,
'teaser' => $teaser,
'categories' => implode(',', array_map('intval', $categories)),
];
$output->writeln(sprintf(' + "%s" (slug %s, teaser draft) - pid %d + %d categorie(s) from sibling #%d',
$title, $slug, (int)$sibling['pid'], count($categories), (int)$sibling['uid']));
}
if (!$apply) {
$output->writeln("\nDRY RUN - nothing written. Add --apply, then re-run vitec:import-product-images.");
return Command::SUCCESS;
}
if ($datamap === []) {
$output->writeln('Nothing to create.');
return Command::SUCCESS;
}
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start($datamap, []);
$dataHandler->process_datamap();
if ($dataHandler->errorLog !== []) {
$output->writeln('<error>DataHandler: ' . implode(' | ', $dataHandler->errorLog) . '</error>');
return Command::FAILURE;
}
foreach ($dataHandler->substNEWwithIDs as $newUid) {
$output->writeln(' created #' . (int)$newUid);
}
$output->writeln('<info>Done. Now run vitec:import-product-images (dry-run, then --apply) to attach the assets; replace the teaser drafts once the agency texts arrive.</info>');
return Command::SUCCESS;
}
}