Files
VITEC-website/packages/vitec/Classes/Command/CreateMissingProductsCommand.php
orascheandClaude Fable 5 77661bf0b6 Datapath stories, grid repairs, menu v2 pure, hero placeholders, fec test
Batch of 2026-10-28/29 plus 2026-11-03:

- NEW vitec:import-datapath-stories: 22 story packages from the agency
  dump (docx + Links/ assets) as usecase records with content, quotes,
  images, customer logos, download cards; --repair-quotes normalizes
  attribution lines; asset backfill for existing stories
- /products grid incident repaired (fix_products_grid, fix_graphics_grid,
  check_grid_page): containers unhidden, 46 duplicate cards soft-deleted,
  section order restored; BuildProductPortfolio and
  ImportProductCategoryPages now GUARD against re-runs on pages that
  already carry vitec_cols_% containers
- ' double-encoding fixed: render-pipeline importers write literal
  apostrophes (ENT_NOQUOTES), fix_double_entities literalizes existing rows
- ticket quick wins (ticket_quickwins_20261028), broadcast market page
  cleanup, AttachCardImages market-card handling, SubmarketWorkbook
  META REPAIR, story content/video fixes (fix_story_*)
- ProvideProductCatalog: transitional v1 fields removed (categories
  uid/link, legacyGroupFields, item products teaser) - pure v2 shape
  (ticket 0024-FE); search renderer keeps its teaser
- BuildProductOverviews: heroes get the numbered placeholder background
  (placeholders/pages/<uid>-01.png, wide grey fallback) - backfill on
  existing pages with empty hero_bgimage, attached at creation for new
  pages; existing references always win (ticket 0026-FE); 40 placeholder
  PNGs shipped to fileadmin (not tracked)
- AttachPageMedia: --depth=3 with sysfolders as pass-through parents for
  the overview pages' -08 menu tiles
- NEW migrations/fec_isolation_test.sh: two-user live test proving
  fe_comment ticket isolation (backend verified tight, cross-user
  sighting traced to the FE page-JSON cache, ticket 0027-BE)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-11-03 18:29:56 +01:00

132 lines
6.2 KiB
PHP

<?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.
*
* 2026-10-28: IQS4 and VisionSC-SDI4 added - the fresh Dropbox dump
* delivered their banners, and both datasheets already sit in the pool.
*
* 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.'],
// 2026-10-28 (delivery brought banners; datasheets already in the pool)
['IQS4', 'iqs4', 'image-4k',
'Distribute a single 4K source across multiple displays with a compact 4K splitter.'],
['VisionSC-SDI4', 'visionsc-sdi4', 'visionsc-s2',
'Four-channel SDI capture card for professional video and visualization workflows.'],
];
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;
}
}