- 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
220 lines
11 KiB
HTML
220 lines
11 KiB
HTML
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
|
|
data-namespace-typo3-fluid="true">
|
|
|
|
<f:layout name="Backend/Default" />
|
|
|
|
<f:section name="main">
|
|
|
|
<div class="module-docheader-bar module-docheader-bar-navigation">
|
|
<div class="module-docheader-bar-column-left">
|
|
<h2 class="t3js-title-inlineedit">VITEC Import — {model.label}</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Model tabs. Products left the CSV flow on 2026-08-21: its tab now leads to
|
|
the XLSX product-text overview (ProductTextImportController); the CSV code
|
|
path for products stays functional but is no longer linked. -->
|
|
<ul class="nav nav-tabs" style="margin-bottom:1rem;">
|
|
<f:for each="{models}" as="m">
|
|
<f:if condition="{m.key} != 'product'">
|
|
<li class="nav-item">
|
|
<a class="nav-link {f:if(condition: '{m.key} == {model.key}', then: 'active')}"
|
|
href="{f:be.uri(route: 'web_vitecimport', parameters: {model: m.key})}">{m.label}</a>
|
|
</li>
|
|
</f:if>
|
|
</f:for>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="{f:be.uri(route: 'web_vitecimport.products')}">Products</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="{f:be.uri(route: 'web_vitecimport.submarkets')}">Sub-Markets</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="{f:be.uri(route: 'web_vitecimport.subsolutions')}">Sub-Solutions</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="{f:be.uri(route: 'web_vitecimport.seo')}">SEO Research</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Upload -->
|
|
<div class="card" style="margin-bottom:1rem;">
|
|
<div class="card-body">
|
|
<form action="{f:be.uri(route: 'web_vitecimport.process')}" method="post" enctype="multipart/form-data" class="row row-cols-auto align-items-center g-2">
|
|
<input type="hidden" name="model" value="{model.key}" />
|
|
<div class="col">
|
|
<input type="file" name="csvfile" accept=".csv,text/csv" class="form-control" required="required" />
|
|
</div>
|
|
<div class="col">
|
|
<button type="submit" name="op" value="preview" class="btn btn-primary">Load CSV</button>
|
|
</div>
|
|
<div class="col form-text">
|
|
Delimiter (";" or ",") and encoding (UTF-8 / Windows-1252) are detected automatically.
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<f:if condition="{hasCsv}">
|
|
|
|
<form action="{f:be.uri(route: 'web_vitecimport.process')}" method="post" id="vitecImportForm">
|
|
<input type="hidden" name="model" value="{model.key}" />
|
|
<input type="hidden" name="csvstate" value="{csvstate}" />
|
|
|
|
<!-- Mapping -->
|
|
<div class="card" style="margin-bottom:1rem;">
|
|
<div class="card-header">
|
|
<strong>Field mapping</strong>
|
|
<f:if condition="{mappingSaved}"><span class="vitec-badge vitec-badge-info">saved mapping loaded</span></f:if>
|
|
</div>
|
|
<div class="card-body">
|
|
<table class="table table-striped table-sm" style="max-width:900px;">
|
|
<thead>
|
|
<tr>
|
|
<th>CSV column</th>
|
|
<th>Sample (row 1)</th>
|
|
<th>Database field</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<f:for each="{columns}" as="col">
|
|
<tr>
|
|
<td><code>{col.name}</code></td>
|
|
<td class="text-muted" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{col.sample}</td>
|
|
<td>
|
|
<select name="map[{col.name}]" class="form-select form-select-sm">
|
|
<option value="">— ignore —</option>
|
|
<f:for each="{fields}" as="label" key="field">
|
|
<option value="{field}" {f:if(condition: '{col.target} == {field}', then: 'selected')}>{field} ({label})</option>
|
|
</f:for>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
</f:for>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="row row-cols-auto align-items-center g-2">
|
|
<div class="col"><label for="identity"><strong>Match records by</strong></label></div>
|
|
<div class="col">
|
|
<select name="identity" id="identity" class="form-select form-select-sm">
|
|
<f:for each="{fields}" as="label" key="field">
|
|
<option value="{field}" {f:if(condition: '{identity} == {field}', then: 'selected')}>{field}</option>
|
|
</f:for>
|
|
</select>
|
|
</div>
|
|
<div class="col">
|
|
<button type="submit" name="op" value="preview" class="btn btn-default">Re-apply mapping</button>
|
|
</div>
|
|
<div class="col">
|
|
<button type="submit" name="op" value="saveMapping" class="btn btn-default">Save mapping</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unified list -->
|
|
<div class="card" style="margin-bottom:1rem;">
|
|
<div class="card-header">
|
|
<strong>Records</strong>
|
|
<span class="vitec-badge vitec-badge-new">{counts.new} new</span>
|
|
<span class="vitec-badge vitec-badge-update">{counts.update} update</span>
|
|
<span class="vitec-badge vitec-badge-unchanged">{counts.unchanged} unchanged</span>
|
|
<span class="vitec-badge vitec-badge-dbonly">{counts.dbonly} only in DB</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<table class="table table-striped table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:30px;"><input type="checkbox" onclick="document.querySelectorAll('.vitec-row-check').forEach(c => c.checked = this.checked);" /></th>
|
|
<th style="width:110px;">Status</th>
|
|
<th style="width:240px;">Record</th>
|
|
<th>Import data (editable JSON — this is what gets written)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<f:for each="{union}" as="row">
|
|
<tr>
|
|
<td>
|
|
<f:if condition="{row.status} != 'dbonly'">
|
|
<input type="checkbox" class="vitec-row-check" name="rows[{row.index}][selected]" value="1" {f:if(condition: row.checked, then: 'checked')} />
|
|
</f:if>
|
|
</td>
|
|
<td>
|
|
<f:switch expression="{row.status}">
|
|
<f:case value="new"><span class="vitec-badge vitec-badge-new">new</span></f:case>
|
|
<f:case value="update"><span class="vitec-badge vitec-badge-update">update</span></f:case>
|
|
<f:case value="unchanged"><span class="vitec-badge vitec-badge-unchanged">unchanged</span></f:case>
|
|
<f:defaultCase><span class="vitec-badge vitec-badge-dbonly">only in DB</span></f:defaultCase>
|
|
</f:switch>
|
|
<f:if condition="{row.viaAlias}"><br /><span class="vitec-badge vitec-badge-info">linked</span></f:if>
|
|
</td>
|
|
<td>
|
|
<strong>{row.label}</strong>
|
|
<f:if condition="{row.uid}"><br /><small class="text-muted">uid {row.uid}</small></f:if>
|
|
<f:if condition="{row.status} == 'update'">
|
|
<br /><small class="text-muted">changes: {row.diff}</small>
|
|
</f:if>
|
|
<f:if condition="{row.aliasable}">
|
|
<select name="alias[{row.index}]" class="form-select form-select-sm" style="margin-top:4px;">
|
|
<option value="0">— not linked —</option>
|
|
<f:for each="{aliasCandidates}" as="cand">
|
|
<option value="{cand.uid}" {f:if(condition: '{cand.uid} == {row.aliasUid}', then: 'selected')}>{cand.title} (uid {cand.uid})</option>
|
|
</f:for>
|
|
</select>
|
|
</f:if>
|
|
</td>
|
|
<td>
|
|
<f:if condition="{row.status} != 'dbonly'">
|
|
<input type="hidden" name="rows[{row.index}][uid]" value="{row.uid}" />
|
|
<textarea name="rows[{row.index}][payload]" class="form-control font-monospace" rows="4" style="font-size:11px;">{row.payload}</textarea>
|
|
</f:if>
|
|
</td>
|
|
</tr>
|
|
</f:for>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="row row-cols-auto align-items-center g-2">
|
|
<div class="col"><label for="pid"><strong>Storage pid for new records</strong></label></div>
|
|
<div class="col"><input type="number" name="pid" id="pid" value="{pid}" class="form-control form-control-sm" style="width:110px;" /></div>
|
|
<div class="col">
|
|
<button type="submit" name="op" value="saveAliases" class="btn btn-default">Save record links</button>
|
|
</div>
|
|
<div class="col">
|
|
<button type="submit" name="op" value="apply" class="btn btn-primary"
|
|
onclick="return confirm('Apply the checked rows to the database?');">Apply checked rows</button>
|
|
</div>
|
|
<div class="col form-text">
|
|
A "new" row that exists in TYPO3 under a different name: pick the record in
|
|
its select, then "Save record links" - the link is stored per model and
|
|
re-applied on every upload.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
</f:if>
|
|
|
|
<f:if condition="!{hasCsv}">
|
|
<f:if condition="{union}">
|
|
<div class="card">
|
|
<div class="card-header"><strong>Current records ({counts.dbonly})</strong></div>
|
|
<div class="card-body">
|
|
<table class="table table-striped table-sm" style="max-width:700px;">
|
|
<thead><tr><th>uid</th><th>Title</th></tr></thead>
|
|
<tbody>
|
|
<f:for each="{union}" as="row">
|
|
<tr><td>{row.uid}</td><td>{row.label}</td></tr>
|
|
</f:for>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</f:if>
|
|
</f:if>
|
|
|
|
</f:section>
|
|
</html>
|