Files
VITEC-website/packages/vitec/Resources/Private/Templates/Import/Index.html
Oliver Rasche 5e3be3ec42 Backend module: CSV import for the VITEC domain models
New "VITEC Import" module under Web: one import page per domain model
(v1: Market, Solution, Product - registry-driven, adding a model is one
config entry). Workflow: upload a CSV (delimiter and encoding are
auto-detected, including German-Excel semicolon/Windows-1252), map CSV
columns to DB fields, persist the mapping per model together with the
identity field used for matching (new table tx_vitec_import_mapping,
no TCA - pure tool configuration), review a unified list of CSV rows
matched against the DB records (new / update with differing fields /
unchanged / db-only), then apply the checked rows through DataHandler.

Each importable row carries an editable JSON payload textarea - what
is written is the textarea content, not the raw CSV, so editors can
fix values right in the review step. The parsed CSV travels through
the form as a hidden JSON field: no session state, no temp files.
Importable fields are derived from TCA at runtime (scalar types only;
files, categories and other relations are excluded - a flat CSV
cannot carry them). Payloads are whitelisted against that field list
on apply; new records require a storage pid (prefilled from existing
records). BE user permissions apply via DataHandler.

The module ships its own CSS (backend-import.css, loaded only by
this module) using the frontend button palette from _vitec.scss:
orange #f47937 for primary actions, navy #26358c for secondary
actions and structure. The stray <h2>Hi</h2> debug leftover in the
shared backend layout is removed (also affects the OG Image module).

Deliberately out of v1: import log with three-way compare (protection
against overwriting manual edits), images/relations, multiple saved
mappings per model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:59:49 +02:00

187 lines
8.9 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 &mdash; {model.label}</h2>
</div>
</div>
<!-- Model tabs -->
<ul class="nav nav-tabs" style="margin-bottom:1rem;">
<f:for each="{models}" as="m">
<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:for>
</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="">&mdash; ignore &mdash;</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>
&nbsp; <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 &mdash; 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>
</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>
</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="apply" class="btn btn-primary"
onclick="return confirm('Apply the checked rows to the database?');">Apply checked rows</button>
</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>