Sub-solution workbook pipeline on layout 5, fix element order in both importers
- New, deliberately separate pipeline for sub-solutions (markets and solutions must never break each other): generator vitec:generate-subsolution-workbooks (list from the page tree below /solutions, 39 workbooks, every sub-solution matched to its record), SubsolutionWorkbookImporter + CLI vitec:import-subsolution-workbooks, and a "Sub-Solutions" tab in the VITEC Import module with its own inbox migrations/subsolutions_xlsx_in/ - Targets the existing template 5 "Solution Detail" without changing it: hero element in colPos 1, the remaining sections in fixed order in colPos 0, found again by CType on re-import; existing empty sub-solution pages are switched to layout 5, missing ones created hidden; detail_page set on the solution record - Workbook per the Figma "07 Solution Detail page": hero without video teaser, intro with headline and without learn-more/portfolio, extended icon list, own page-layout reference image - Shared content elements: market hero/intro/insights relabelled "Market/Solution", market intro gains the optional intro_headline field (new column, requires extension:setup) - Fix: DataHandler puts every NEW record with a positive pid at the top of its column, so pages built in one run came out reversed. Both importers now track the page order per colPos and write tt_content.sorting explicitly after the DataHandler run; re-applying repairs pages imported earlier (verified on the control-room sub-solution and sub-market page 443) - migrations/fill_test_subsolution.php: realistic test content from vitec.com copy and images for the control-room sub-solution - Docs section 5z
This commit is contained in:
1 parent
3d5459b8d2
commit
8e67f5db23
84 files changed
+1772
-35
No files matched your search
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Command;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
|
||||
use PhpOffice\PhpSpreadsheet\NamedRange;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Protection;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
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\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Generates one customer-facing XLSX per SUB-SOLUTION for the solution
|
||||
* detail template (frontend layout 5, Figma "07 Solution Detail page").
|
||||
*
|
||||
* DELIBERATE SIBLING of GenerateSubmarketWorkbooksCommand, NOT a shared
|
||||
* implementation (decision 2026-10-05): markets and solutions evolve
|
||||
* independently, changing one must never break the other. Differences from
|
||||
* the sub-market workbook:
|
||||
*
|
||||
* - source of the group/sub list is the PAGE TREE below /solutions (the
|
||||
* sub-solution pages already exist as empty shells), not a category tree
|
||||
* - hero has NO video teaser, intro has a HEADLINE and NO learn-more /
|
||||
* portfolio rows (per the Figma solution design)
|
||||
* - icon list extended by the solution set (distribute, scale, respond,
|
||||
* view) - icon artwork still pending with the front-end team
|
||||
* - meta names: meta_subsolution / meta_group / meta_record_uid
|
||||
*
|
||||
* Everything else matches the proven sub-market mechanics: named ranges on
|
||||
* every input, locked sheet with yellow input cells, dropdowns live from
|
||||
* the DB, hidden Meta sheet as the importer's ground truth.
|
||||
*
|
||||
* Usage:
|
||||
* vendor/bin/typo3 vitec:generate-subsolution-workbooks
|
||||
* vendor/bin/typo3 vitec:generate-subsolution-workbooks --only=iptv
|
||||
* vendor/bin/typo3 vitec:generate-subsolution-workbooks --out=/tmp/xls
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'vitec:generate-subsolution-workbooks',
|
||||
description: 'Generate the customer XLSX template per sub-solution (dropdowns live from the DB)'
|
||||
)]
|
||||
final class GenerateSubsolutionWorkbooksCommand extends Command
|
||||
{
|
||||
private const CARD_ROWS = 8;
|
||||
private const STORY_ROWS = 6;
|
||||
private const PRODUCT_ROWS = 9;
|
||||
|
||||
/** Provisional flip-card icon set - extend once agreed with the front end. */
|
||||
private const ICONS = ['capture', 'process', 'manage', 'deliver', 'engage', 'distribute', 'scale', 'respond', 'view'];
|
||||
|
||||
private const COLOR_SECTION = 'FF1F3A93';
|
||||
private const COLOR_NAVY = 'FF16227E';
|
||||
private const COLOR_ORANGE = 'FFF36F21';
|
||||
private const COLOR_INPUT = 'FFFFF8E1';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('out', null, InputOption::VALUE_REQUIRED, 'Output directory', 'migrations/subsolutions_xlsx_out');
|
||||
$this->addOption('only', null, InputOption::VALUE_REQUIRED, 'Only sub-solutions whose title contains this substring');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
Bootstrap::initializeBackendAuthentication();
|
||||
|
||||
$outDir = rtrim((string)$input->getOption('out'), '/');
|
||||
$only = mb_strtolower((string)($input->getOption('only') ?? ''));
|
||||
|
||||
$groups = $this->solutionGroups();
|
||||
if ($groups === []) {
|
||||
$output->writeln('<error>No sub-solution pages found below /solutions.</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$stories = $this->titles('tx_vitec_domain_model_usecase', false);
|
||||
$products = $this->titles('tx_vitec_domain_model_product', true);
|
||||
$recordByTitle = $this->solutionRecordsByNormTitle();
|
||||
|
||||
$count = 0;
|
||||
foreach ($groups as $group) {
|
||||
foreach ($group['children'] as $subsolution) {
|
||||
if ($only !== '' && !str_contains(mb_strtolower($subsolution), $only)) {
|
||||
continue;
|
||||
}
|
||||
$record = $recordByTitle[$this->norm($subsolution)] ?? null;
|
||||
$dir = $outDir . '/' . $this->fileSafe($group['title']);
|
||||
GeneralUtility::mkdir_deep($dir);
|
||||
$file = $dir . '/' . $this->fileSafe($subsolution) . '.xlsx';
|
||||
$this->writeWorkbook($file, $subsolution, $group['title'], $record, $stories, $products);
|
||||
$output->writeln(sprintf('%s (record: %s)', $file, $record ? '#' . $record['uid'] : '<comment>KEINER</comment>'));
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
$output->writeln(sprintf("\n%d workbook(s) written to %s/", $count, $outDir));
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{uid:int,title:string}|null $record
|
||||
* @param string[] $stories
|
||||
* @param string[] $products
|
||||
*/
|
||||
private function writeWorkbook(string $file, string $subsolution, string $group, ?array $record, array $stories, array $products): void
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
||||
// ---- Instructions (VITEC look) --------------------------------------
|
||||
$info = $spreadsheet->getActiveSheet();
|
||||
$info->setTitle('Read me');
|
||||
$info->getColumnDimension('A')->setWidth(3);
|
||||
$info->getColumnDimension('B')->setWidth(100);
|
||||
$info->getColumnDimension('C')->setWidth(3);
|
||||
$info->setShowGridlines(false);
|
||||
|
||||
$info->mergeCells('A1:C3');
|
||||
$info->setCellValue('A1', 'VITEC · Solution Detail Page');
|
||||
$info->getStyle('A1:C3')->getFill()->setFillType('solid')->getStartColor()->setARGB(self::COLOR_NAVY);
|
||||
$info->getStyle('A1')->getFont()->setBold(true)->setSize(20)->getColor()->setARGB('FFFFFFFF');
|
||||
$info->getStyle('A1:C3')->getAlignment()->setVertical(Alignment::VERTICAL_CENTER)->setIndent(1);
|
||||
$info->mergeCells('A4:C4');
|
||||
$info->getStyle('A4:C4')->getFill()->setFillType('solid')->getStartColor()->setARGB(self::COLOR_ORANGE);
|
||||
$info->getRowDimension(4)->setRowHeight(4);
|
||||
|
||||
$info->mergeCells('A6:C6');
|
||||
$info->setCellValue('A6', 'This workbook feeds exactly ONE page: ' . $subsolution . ' (solution area: ' . $group . ')');
|
||||
$info->getStyle('A6')->getFont()->setBold(true)->setSize(12)->getColor()->setARGB(self::COLOR_NAVY);
|
||||
|
||||
$infoBlocks = [
|
||||
['HOW TO FILL IT IN', [
|
||||
'Only the yellow cells are editable - everything else is locked on purpose.',
|
||||
'Cells with a dropdown arrow: please PICK from the list, do not type.',
|
||||
'Character limits are design hints, not hard stops - you may see a friendly warning.',
|
||||
'Rows you do not need (cards, stories, products): simply leave them empty.',
|
||||
]],
|
||||
['IMAGES & VIDEOS', [
|
||||
'These fields take a URL (Dropbox, WeTransfer, any web link).',
|
||||
'We download, review and crop everything on our side - your page goes live',
|
||||
'even if an image is still pending.',
|
||||
'The Industry Insights video card is optional: leave the video URL empty',
|
||||
'and the whole card simply does not appear.',
|
||||
]],
|
||||
['YOUR REFERENCE', [
|
||||
'The sheet "Page layout" shows the final page design with every section labelled -',
|
||||
'the section numbers there match the blocks in the "Content" sheet.',
|
||||
]],
|
||||
['WHEN YOU ARE DONE', [
|
||||
'Send the file back. It is imported automatically, and we review every page',
|
||||
'before it goes live.',
|
||||
]],
|
||||
];
|
||||
$infoRow = 8;
|
||||
foreach ($infoBlocks as [$blockTitle, $blockLines]) {
|
||||
$info->setCellValue('B' . $infoRow, $blockTitle);
|
||||
$info->getStyle('B' . $infoRow)->getFont()->setBold(true)->setSize(11)->getColor()->setARGB(self::COLOR_ORANGE);
|
||||
$infoRow++;
|
||||
foreach ($blockLines as $line) {
|
||||
$info->setCellValue('B' . $infoRow, $line);
|
||||
$infoRow++;
|
||||
}
|
||||
$infoRow++;
|
||||
}
|
||||
$info->getProtection()->setSheet(true);
|
||||
|
||||
// ---- Page layout reference (Figma screenshot) -----------------------
|
||||
$referencePng = GeneralUtility::getFileAbsFileName('EXT:vitec/Resources/Private/SubmarketTemplate/subsolution-reference.png');
|
||||
if (is_file($referencePng)) {
|
||||
$layout = $spreadsheet->createSheet();
|
||||
$layout->setTitle('Page layout');
|
||||
$layout->setShowGridlines(false);
|
||||
$layout->getColumnDimension('A')->setWidth(34);
|
||||
$layout->setCellValue('A1', 'This is how the page will look.');
|
||||
$layout->getStyle('A1')->getFont()->setBold(true)->setSize(12)->getColor()->setARGB(self::COLOR_NAVY);
|
||||
$layout->setCellValue('A2', 'Labels match the Content sheet.');
|
||||
$labels = [4 => 'SECTION 1 - HERO', 11 => 'SECTION 2 - INTRO', 21 => 'SECTION 3 - ADVANTAGE CARDS', 32 => 'SECTION 4 - TESTIMONIALS', 42 => 'SECTION 5 - PRODUCTS', 55 => 'SECTION 6 - INDUSTRY INSIGHTS'];
|
||||
foreach ($labels as $labelRow => $label) {
|
||||
$layout->setCellValue('A' . $labelRow, $label . ' →');
|
||||
$layout->getStyle('A' . $labelRow)->getFont()->setBold(true)->setSize(9)->getColor()->setARGB(self::COLOR_ORANGE);
|
||||
}
|
||||
$drawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
|
||||
$drawing->setName('Page design');
|
||||
$drawing->setPath($referencePng);
|
||||
$drawing->setCoordinates('B2');
|
||||
$drawing->setWorksheet($layout);
|
||||
$layout->getProtection()->setSheet(true);
|
||||
}
|
||||
|
||||
// ---- Content sheet --------------------------------------------------
|
||||
$sheet = $spreadsheet->createSheet();
|
||||
$sheet->setTitle('Content');
|
||||
$sheet->getColumnDimension('A')->setWidth(28);
|
||||
$sheet->getColumnDimension('B')->setWidth(64);
|
||||
$sheet->getColumnDimension('C')->setWidth(22);
|
||||
$sheet->getColumnDimension('D')->setWidth(64);
|
||||
$sheet->getProtection()->setSheet(true);
|
||||
$sheet->getStyle('A1:D200')->getAlignment()->setVertical(Alignment::VERTICAL_TOP);
|
||||
$sheet->getStyle('B1:B200')->getAlignment()->setWrapText(true);
|
||||
$sheet->getStyle('D1:D200')->getAlignment()->setWrapText(true);
|
||||
|
||||
$row = 1;
|
||||
$sheet->setCellValue('A' . $row, 'Sub-solution');
|
||||
$sheet->setCellValue('B' . $row, $subsolution);
|
||||
$sheet->setCellValue('C' . $row, 'Solution area');
|
||||
$sheet->setCellValue('D' . $row, $group);
|
||||
$sheet->getStyle('A1:D1')->getFont()->setBold(true);
|
||||
$row += 2;
|
||||
|
||||
// Section 1 - Hero (no video teaser in the solution design)
|
||||
$row = $this->sectionHeader($sheet, $row, 'SECTION 1 - HERO (the page title is set by us)');
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Subline *', 'hero_subline', 'max ~90 characters', 120);
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'CTA button label', 'hero_cta_label', 'empty = "Get in touch"', 40);
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'CTA button link (URL)', 'hero_cta_url', 'empty = /contact');
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Background image URL', 'hero_bgimage_url', '1920x600, dark; we review');
|
||||
$row++;
|
||||
|
||||
// Section 2 - Intro (headline + longer text, image right)
|
||||
$row = $this->sectionHeader($sheet, $row, 'SECTION 2 - INTRO');
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Headline', 'intro_headline', 'max ~90 characters', 120);
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Intro text *', 'intro_text', 'max ~700 characters, plain text', 900, 4);
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Image URL (right)', 'intro_image_url', '636x469 (4:3); we review');
|
||||
$row++;
|
||||
|
||||
// Section 3 - Advantages (flip cards)
|
||||
$row = $this->sectionHeader($sheet, $row, 'SECTION 3 - ADVANTAGE FLIP CARDS');
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Headline *', 'flow_headline', 'max ~70 characters', 90);
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Subtext', 'flow_subtext', 'max ~230 characters', 300, 2);
|
||||
$sheet->setCellValue('A' . $row, 'Cards - one per row, leave surplus rows empty:');
|
||||
$sheet->getStyle('A' . $row)->getFont()->setItalic(true);
|
||||
$row++;
|
||||
$sheet->setCellValue('A' . $row, 'Icon');
|
||||
$sheet->setCellValue('B' . $row, 'Title (short, e.g. "Distribute")');
|
||||
$sheet->setCellValue('C' . $row, 'Front text (~80 characters)');
|
||||
$sheet->setCellValue('D' . $row, 'Back text - flip side (~200 characters)');
|
||||
$sheet->getStyle('A' . $row . ':D' . $row)->getFont()->setBold(true);
|
||||
$row++;
|
||||
for ($i = 1; $i <= self::CARD_ROWS; $i++) {
|
||||
$this->unlock($sheet, 'A' . $row);
|
||||
$this->dropdown($sheet, 'A' . $row, 'Icons');
|
||||
$this->name($spreadsheet, $sheet, 'flow_card' . $i . '_icon', 'A' . $row);
|
||||
$this->unlock($sheet, 'B' . $row);
|
||||
$this->name($spreadsheet, $sheet, 'flow_card' . $i . '_title', 'B' . $row);
|
||||
$this->unlock($sheet, 'C' . $row);
|
||||
$this->name($spreadsheet, $sheet, 'flow_card' . $i . '_front', 'C' . $row);
|
||||
$sheet->getColumnDimension('C')->setWidth(40);
|
||||
$this->unlock($sheet, 'D' . $row);
|
||||
$this->name($spreadsheet, $sheet, 'flow_card' . $i . '_back', 'D' . $row);
|
||||
$row++;
|
||||
}
|
||||
$row++;
|
||||
|
||||
// Section 4 - Testimonials
|
||||
$row = $this->sectionHeader($sheet, $row, 'SECTION 4 - TESTIMONIALS (pick case studies; quotes are maintained by VITEC)');
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Headline', 'testi_headline', 'empty = "Testimonials from the Field"', 90);
|
||||
for ($i = 1; $i <= self::STORY_ROWS; $i++) {
|
||||
$sheet->setCellValue('A' . $row, 'Case study ' . $i);
|
||||
$this->unlock($sheet, 'B' . $row);
|
||||
$this->dropdown($sheet, 'B' . $row, 'Stories');
|
||||
$this->name($spreadsheet, $sheet, 'testi_story_' . $i, 'B' . $row);
|
||||
$row++;
|
||||
}
|
||||
$row++;
|
||||
|
||||
// Section 5 - Products
|
||||
$row = $this->sectionHeader($sheet, $row, 'SECTION 5 - PRODUCTS (pick from the list; all copy comes from our product data)');
|
||||
$sheet->setCellValue('A' . $row, 'Featured product *');
|
||||
$this->unlock($sheet, 'B' . $row);
|
||||
$this->dropdown($sheet, 'B' . $row, 'Products');
|
||||
$this->name($spreadsheet, $sheet, 'prod_featured', 'B' . $row);
|
||||
$row++;
|
||||
for ($i = 1; $i <= self::PRODUCT_ROWS; $i++) {
|
||||
$sheet->setCellValue('A' . $row, 'Product card ' . $i);
|
||||
$this->unlock($sheet, 'B' . $row);
|
||||
$this->dropdown($sheet, 'B' . $row, 'Products');
|
||||
$this->name($spreadsheet, $sheet, 'prod_card_' . $i, 'B' . $row);
|
||||
$row++;
|
||||
}
|
||||
$row++;
|
||||
|
||||
// Section 6 - Insights
|
||||
$row = $this->sectionHeader($sheet, $row, 'SECTION 6 - INDUSTRY INSIGHTS');
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Paragraph 1', 'insights_text1', 'max ~250 characters', 300, 2);
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Paragraph 2', 'insights_text2', 'max ~250 characters', 300, 2);
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'CTA label', 'insights_cta_label', 'empty = no button', 40);
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'CTA target', 'insights_cta_target', 'anchor or URL; empty = /success-stories', 0, 1, 'Anchors');
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Video card: person', 'insights_video_person', 'optional', 80);
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Video card: role / title', 'insights_video_role', '', 120);
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Video card: video title', 'insights_video_title', '', 120);
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Video card: video URL', 'insights_video_url', 'empty = no video card');
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'Video card: cover image URL', 'insights_video_cover_url', '');
|
||||
$row++;
|
||||
|
||||
// SEO
|
||||
$row = $this->sectionHeader($sheet, $row, 'SEO (optional - we compose sensible defaults if empty)');
|
||||
$row = $this->inputRow($spreadsheet, $sheet, $row, 'SEO title', 'seo_title', 'ca. 60 characters', 80);
|
||||
$this->inputRow($spreadsheet, $sheet, $row, 'SEO description', 'seo_description', 'ca. 155 characters', 200, 2);
|
||||
|
||||
// ---- Lists (hidden) -------------------------------------------------
|
||||
$lists = $spreadsheet->createSheet();
|
||||
$lists->setTitle('Lists');
|
||||
$this->fillList($spreadsheet, $lists, 'A', 'Stories', $stories);
|
||||
$this->fillList($spreadsheet, $lists, 'B', 'Products', $products);
|
||||
$this->fillList($spreadsheet, $lists, 'C', 'Icons', self::ICONS);
|
||||
$this->fillList($spreadsheet, $lists, 'D', 'Anchors', ['workflow', 'testimonials', 'products', 'insights']);
|
||||
$lists->setSheetState(Worksheet::SHEETSTATE_HIDDEN);
|
||||
|
||||
// ---- Meta (hidden) - the importer's ground truth --------------------
|
||||
$meta = $spreadsheet->createSheet();
|
||||
$meta->setTitle('Meta');
|
||||
$meta->setCellValue('A1', 'subsolution');
|
||||
$meta->setCellValue('B1', $subsolution);
|
||||
$meta->setCellValue('A2', 'group');
|
||||
$meta->setCellValue('B2', $group);
|
||||
$meta->setCellValue('A3', 'record_uid');
|
||||
$meta->setCellValue('B3', $record ? (int)$record['uid'] : 0);
|
||||
$meta->setCellValue('A4', 'generated');
|
||||
$meta->setCellValue('B4', date('Y-m-d H:i'));
|
||||
$meta->setCellValue('A5', 'template_version');
|
||||
$meta->setCellValue('B5', '1');
|
||||
foreach (['subsolution' => 'B1', 'group' => 'B2', 'record_uid' => 'B3', 'template_version' => 'B5'] as $metaName => $cell) {
|
||||
$spreadsheet->addNamedRange(new NamedRange('meta_' . $metaName, $meta, '$' . substr($cell, 0, 1) . '$' . substr($cell, 1)));
|
||||
}
|
||||
$meta->setSheetState(Worksheet::SHEETSTATE_HIDDEN);
|
||||
|
||||
$spreadsheet->setActiveSheetIndex(1); // Content
|
||||
(new Xlsx($spreadsheet))->save($file);
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
// ---- sheet helpers ------------------------------------------------------
|
||||
|
||||
private function sectionHeader(Worksheet $sheet, int $row, string $title): int
|
||||
{
|
||||
$sheet->setCellValue('A' . $row, $title);
|
||||
$sheet->mergeCells('A' . $row . ':D' . $row);
|
||||
$style = $sheet->getStyle('A' . $row);
|
||||
$style->getFont()->setBold(true)->getColor()->setARGB('FFFFFFFF');
|
||||
$style->getFill()->setFillType('solid')->getStartColor()->setARGB(self::COLOR_SECTION);
|
||||
return $row + 1;
|
||||
}
|
||||
|
||||
private function inputRow(
|
||||
Spreadsheet $spreadsheet,
|
||||
Worksheet $sheet,
|
||||
int $row,
|
||||
string $label,
|
||||
string $rangeName,
|
||||
string $hint,
|
||||
int $maxLength = 0,
|
||||
int $rowHeightLines = 1,
|
||||
string $listName = ''
|
||||
): int {
|
||||
$sheet->setCellValue('A' . $row, $label);
|
||||
$this->unlock($sheet, 'B' . $row);
|
||||
if ($listName !== '') {
|
||||
$this->dropdown($sheet, 'B' . $row, $listName, $listName === 'Anchors');
|
||||
}
|
||||
if ($hint !== '') {
|
||||
$sheet->setCellValue('C' . $row, $hint);
|
||||
$sheet->getStyle('C' . $row)->getFont()->setItalic(true)->setSize(9);
|
||||
}
|
||||
if ($maxLength > 0) {
|
||||
$validation = $sheet->getCell('B' . $row)->getDataValidation();
|
||||
$validation->setType(DataValidation::TYPE_TEXTLENGTH)
|
||||
->setOperator(DataValidation::OPERATOR_LESSTHANOREQUAL)
|
||||
->setFormula1((string)$maxLength)
|
||||
->setAllowBlank(true)
|
||||
->setShowErrorMessage(true)
|
||||
->setErrorStyle(DataValidation::STYLE_WARNING)
|
||||
->setErrorTitle('A bit long')
|
||||
->setError('The design is tuned for shorter copy here - this may get cropped.');
|
||||
}
|
||||
if ($rowHeightLines > 1) {
|
||||
$sheet->getRowDimension($row)->setRowHeight(15 * $rowHeightLines);
|
||||
}
|
||||
$this->name($spreadsheet, $sheet, $rangeName, 'B' . $row);
|
||||
return $row + 1;
|
||||
}
|
||||
|
||||
private function unlock(Worksheet $sheet, string $cell): void
|
||||
{
|
||||
$sheet->getStyle($cell)->getProtection()->setLocked(Protection::PROTECTION_UNPROTECTED);
|
||||
$sheet->getStyle($cell)->getFill()->setFillType('solid')->getStartColor()->setARGB(self::COLOR_INPUT);
|
||||
}
|
||||
|
||||
private function dropdown(Worksheet $sheet, string $cell, string $listName, bool $allowFreeText = false): void
|
||||
{
|
||||
$validation = $sheet->getCell($cell)->getDataValidation();
|
||||
$validation->setType(DataValidation::TYPE_LIST)
|
||||
->setFormula1($listName)
|
||||
->setAllowBlank(true)
|
||||
->setShowDropDown(true)
|
||||
->setShowErrorMessage(!$allowFreeText)
|
||||
->setErrorStyle($allowFreeText ? DataValidation::STYLE_INFORMATION : DataValidation::STYLE_STOP)
|
||||
->setErrorTitle('Please pick from the list')
|
||||
->setError($allowFreeText ? 'Anchors come from the list - URLs may be typed freely.' : 'This field only accepts values from the dropdown list.');
|
||||
}
|
||||
|
||||
/** @param string[] $values */
|
||||
private function fillList(Spreadsheet $spreadsheet, Worksheet $lists, string $column, string $name, array $values): void
|
||||
{
|
||||
$rowIndex = 1;
|
||||
foreach ($values as $value) {
|
||||
$lists->setCellValue($column . $rowIndex, $value);
|
||||
$rowIndex++;
|
||||
}
|
||||
$last = max(1, $rowIndex - 1);
|
||||
$spreadsheet->addNamedRange(new NamedRange($name, $lists, '$' . $column . '$1:$' . $column . '$' . $last));
|
||||
}
|
||||
|
||||
private function name(Spreadsheet $spreadsheet, Worksheet $sheet, string $rangeName, string $cell): void
|
||||
{
|
||||
preg_match('/^([A-Z]+)(\d+)$/', $cell, $m);
|
||||
$spreadsheet->addNamedRange(new NamedRange($rangeName, $sheet, '$' . $m[1] . '$' . $m[2]));
|
||||
}
|
||||
|
||||
// ---- data ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Solution areas with their sub-solutions, read from the PAGE TREE below
|
||||
* /solutions - the sub-solution pages already exist as (empty) shells,
|
||||
* so the tree is the ground truth here (markets use the category tree).
|
||||
*
|
||||
* @return array<int,array{title:string,children:string[]}>
|
||||
*/
|
||||
private function solutionGroups(): array
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
|
||||
$root = $connection->fetchAssociative("SELECT uid FROM pages WHERE deleted = 0 AND slug = '/solutions'");
|
||||
if (!$root) {
|
||||
return [];
|
||||
}
|
||||
$groups = [];
|
||||
$mains = $connection->fetchAllAssociative(
|
||||
'SELECT uid, title FROM pages WHERE deleted = 0 AND doktype = 1 AND sys_language_uid IN (0,-1) AND pid = ? ORDER BY sorting',
|
||||
[(int)$root['uid']]
|
||||
);
|
||||
foreach ($mains as $main) {
|
||||
$children = $connection->fetchFirstColumn(
|
||||
'SELECT title FROM pages WHERE deleted = 0 AND doktype = 1 AND sys_language_uid IN (0,-1) AND pid = ? ORDER BY sorting',
|
||||
[(int)$main['uid']]
|
||||
);
|
||||
if ($children !== []) {
|
||||
$groups[] = ['title' => (string)$main['title'], 'children' => array_map('strval', $children)];
|
||||
}
|
||||
}
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/** @return array<string,array{uid:int,title:string}> normalized title => record */
|
||||
private function solutionRecordsByNormTitle(): array
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_vitec_domain_model_solution');
|
||||
$byTitle = [];
|
||||
foreach ($connection->fetchAllAssociative('SELECT uid, title FROM tx_vitec_domain_model_solution WHERE deleted = 0') as $row) {
|
||||
$byTitle[$this->norm((string)$row['title'])] = ['uid' => (int)$row['uid'], 'title' => (string)$row['title']];
|
||||
}
|
||||
return $byTitle;
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
private function titles(string $table, bool $productFilters): array
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
|
||||
$where = 'deleted = 0 AND hidden = 0';
|
||||
if ($productFilters) {
|
||||
$where .= ' AND legacy = 0 AND supportproduct = 0 AND hideonwebsite = 0 AND subproduct = 0';
|
||||
}
|
||||
return array_map('strval', $connection->fetchFirstColumn(
|
||||
'SELECT title FROM ' . $table . ' WHERE ' . $where . ' ORDER BY title'
|
||||
));
|
||||
}
|
||||
|
||||
private function norm(string $value): string
|
||||
{
|
||||
return (string)preg_replace('/[^a-z0-9]+/', '', str_replace('+', 'plus', mb_strtolower($value)));
|
||||
}
|
||||
|
||||
private function fileSafe(string $value): string
|
||||
{
|
||||
$safe = (string)preg_replace('/[^A-Za-z0-9 _&\\-]+/', '', $value);
|
||||
return trim((string)preg_replace('/\s+/', ' ', $safe));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user