feat(csv-import): Add custom row aggregation options in AdminCsvTemplateEditorPage
All checks were successful
Deploy Development / deploy (push) Successful in 55s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 16s

- Introduced a new section for row aggregation settings, allowing users to customize aggregation functions for imported CSV data.
- Implemented functionality for users to save custom aggregation configurations and select key fields for aggregation.
- Enhanced user interface with detailed instructions and options for managing row aggregation, improving overall usability in template management.
This commit is contained in:
Lars 2026-04-10 15:26:59 +02:00
parent a51ee1d304
commit ad7aa2d255

View File

@ -965,6 +965,191 @@ export default function AdminCsvTemplateEditorPage() {
)}
</div>
{!aggregateSleepImport && (
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
<div className="form-label">3a. Zeilenaggregation</div>
{!modMeta?.fields || Object.keys(modMeta.fields).length === 0 ? (
<p style={{ fontSize: 14, color: 'var(--text2)', marginTop: 8 }}>
Modul-Metadaten laden bitte Seite kurz offen lassen oder neu laden.
</p>
) : (
<>
<p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 8, lineHeight: 1.55 }}>
Mehrere CSV-Zeilen mit denselben Werten in den gewählten <strong>Schlüsselfeldern</strong> werden zu einer
importierten Zeile zusammengefasst. Für alle übrigen zugewiesenen Zielfelder gilt{' '}
<strong>eine gemeinsame</strong> Funktion. Textfelder werden bei Summe/Mittelwert usw. automatisch
ausgelassen; mit Erster/Letzter Wert sind sie enthalten.
</p>
<label
style={{
display: 'flex',
alignItems: 'flex-start',
gap: 10,
marginTop: 14,
cursor: 'pointer',
fontSize: 14,
color: 'var(--text1)',
}}
>
<input
type="checkbox"
checked={rowAggUseCustom}
onChange={(e) => {
const on = e.target.checked
setRowAggUseCustom(on)
if (!on) {
setRowAggIrregular(false)
setRowAggGroupBy([])
setRowAggMode('')
setRowAggJsonText('{}')
}
}}
style={{ marginTop: 3 }}
/>
<span>
<strong>Eigene Aggregation in dieser Vorlage speichern.</strong> Wenn deaktiviert, gilt der{' '}
<strong>Modul-Standard</strong> (siehe unten) bzw. kein Aggregat, wenn das Modul keinen definiert.
</span>
</label>
{modMeta.import_row_processing_default && (
<details style={{ marginTop: 12, fontSize: 13, color: 'var(--text2)' }}>
<summary style={{ cursor: 'pointer' }}>Modul-Standard (Referenz, wenn Haken oben aus ist)</summary>
<pre
style={{
marginTop: 8,
padding: 12,
background: 'var(--surface2)',
borderRadius: 8,
overflow: 'auto',
fontSize: 12,
textAlign: 'left',
}}
>
{JSON.stringify(modMeta.import_row_processing_default, null, 2)}
</pre>
</details>
)}
{rowAggUseCustom && (
<>
{modMeta.import_row_processing_default && (
<button
type="button"
className="btn btn-secondary"
style={{ marginTop: 12 }}
onClick={() => {
const d = modMeta.import_row_processing_default
const p = parseStoredImportRowProcessing(d)
setRowAggIrregular(p.irregular)
setRowAggGroupBy(p.groupBy)
setRowAggMode(p.mode)
setRowAggJsonText(JSON.stringify(d, null, 2))
}}
>
Modul-Vorgabe übernehmen
</button>
)}
{rowAggIrregular ? (
<>
<p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 14, lineHeight: 1.55 }}>
Diese Vorlage nutzt <strong>unterschiedliche</strong> Aggregations-Funktionen pro Feld. JSON
anpassen oder vereinheitlichen (pro-Feld-Auswahl folgt in einer späteren Ausbaustufe).
</p>
<textarea
className="form-input"
style={{
width: '100%',
minHeight: 160,
marginTop: 8,
fontFamily: 'monospace',
fontSize: 12,
textAlign: 'left',
}}
value={rowAggJsonText}
onChange={(e) => setRowAggJsonText(e.target.value)}
/>
</>
) : (
<>
<div className="form-label" style={{ marginTop: 16 }}>
Schlüsselfelder (Mehrfachauswahl)
</div>
<p style={{ fontSize: 12, color: 'var(--text3)', marginTop: 6 }}>
Nur bereits zugewiesene Zielfelder (Abschnitt 3).
</p>
{rowAggGroupCandidates.length === 0 ? (
<p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 8 }}>
Noch keine Zielfelder zugewiesen nach Zuweisung erscheinen die Schlüssel hier.
</p>
) : (
<div
style={{
marginTop: 10,
display: 'flex',
flexDirection: 'column',
gap: 8,
alignItems: 'flex-start',
}}
>
{rowAggGroupCandidates.map((key) => (
<label
key={key}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
cursor: 'pointer',
fontSize: 14,
}}
>
<input
type="checkbox"
checked={rowAggGroupBy.includes(key)}
onChange={() => {
setRowAggIrregular(false)
setRowAggGroupBy((prev) =>
prev.includes(key) ? prev.filter((x) => x !== key) : [...prev, key],
)
}}
/>
<code>{key}</code>
</label>
))}
</div>
)}
<label className="form-label" style={{ marginTop: 16, display: 'block' }}>
Funktion für alle übrigen Zielfelder
</label>
<select
className="form-input"
style={{
width: '100%',
maxWidth: 420,
marginTop: 8,
textAlign: 'left',
minHeight: 46,
}}
value={rowAggMode}
onChange={(e) => {
setRowAggIrregular(false)
setRowAggMode(e.target.value)
}}
>
<option value=""> wählen </option>
{ROW_AGG_OPS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</>
)}
</>
)}
</>
)}
</div>
)}
{unitTargets.length > 0 && (
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
<div className="form-label">3b. Quelleinheit (optional)</div>