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>
This commit is contained in:
orascheandClaude Opus 5 committed 2026-08-07 12:59:49 +02:00
1 parent 10c0328bf2
commit 5e3be3ec42
4 files changed
+117 -10

No files matched your search

@@ -49,7 +49,7 @@
<div class="card" style="margin-bottom:1rem;">
<div class="card-header">
<strong>Field mapping</strong>
<f:if condition="{mappingSaved}"><span class="badge bg-info">saved mapping loaded</span></f:if>
<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;">
@@ -101,10 +101,10 @@
<div class="card" style="margin-bottom:1rem;">
<div class="card-header">
<strong>Records</strong>
&nbsp; <span class="badge bg-success">{counts.new} new</span>
<span class="badge bg-warning text-dark">{counts.update} update</span>
<span class="badge bg-secondary">{counts.unchanged} unchanged</span>
<span class="badge bg-light text-dark">{counts.dbonly} only in DB</span>
&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">
@@ -126,10 +126,10 @@
</td>
<td>
<f:switch expression="{row.status}">
<f:case value="new"><span class="badge bg-success">new</span></f:case>
<f:case value="update"><span class="badge bg-warning text-dark">update</span></f:case>
<f:case value="unchanged"><span class="badge bg-secondary">unchanged</span></f:case>
<f:defaultCase><span class="badge bg-light text-dark">only in DB</span></f:defaultCase>
<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>