v4.1.0: DMS email documents, category-specific Nachweis linking, version system
- Save cover email body as DMS document with new 'email' context type - Show email body separately from attachments in email detail view - Add per-category DMS document assignment in quarterly confirmation (Studiennachweis, Einkommenssituation, Vermögenssituation) - Add VERSION file and context processor for automatic version display - Add MCP server, agent system, import/export, and new migrations - Update compose files and production environment template Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
208
app/templates/stiftung/csv_import_mapping.html
Normal file
208
app/templates/stiftung/csv_import_mapping.html
Normal file
@@ -0,0 +1,208 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}CSV Import – Feldzuordnung{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1><i class="fas fa-columns text-primary"></i> Feldzuordnung: {{ import_label }}</h1>
|
||||
<a href="{% url 'stiftung:import_export_hub' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Zurück
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-file-csv"></i>
|
||||
<strong>{{ filename }}</strong> – {{ total_rows }} Datenzeilen erkannt.
|
||||
Ordnen Sie die CSV-Spalten den Datenbankfeldern zu. Nicht zugeordnete Spalten werden übersprungen.
|
||||
</div>
|
||||
|
||||
<form method="post" action="{% url 'stiftung:csv_import_execute' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Mapping Table -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-project-diagram"></i> Spalten zuordnen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 5%;">#</th>
|
||||
<th style="width: 25%;">CSV-Spalte</th>
|
||||
<th style="width: 5%;"></th>
|
||||
<th style="width: 30%;">Zuordnung</th>
|
||||
<th style="width: 35%;">Vorschau</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for col in header_previews %}
|
||||
<tr>
|
||||
<td class="text-muted">{{ forloop.counter }}</td>
|
||||
<td><code>{{ col.header }}</code></td>
|
||||
<td class="text-center text-muted"><i class="fas fa-arrow-right"></i></td>
|
||||
<td>
|
||||
<select name="mapping_{{ forloop.counter0 }}" class="form-select form-select-sm mapping-select"
|
||||
data-col-index="{{ forloop.counter0 }}">
|
||||
<option value="__skip__">– Überspringen –</option>
|
||||
{% for field in model_fields %}
|
||||
<option value="{{ field.1 }}">
|
||||
{{ field.0 }}{% if field.3 %} *{% endif %}
|
||||
({{ field.2 }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">{{ col.preview|truncatechars:60 }}</small>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<small class="text-muted">* = Pflichtfeld. Zuordnungen werden automatisch vorgeschlagen und können manuell geändert werden.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Table -->
|
||||
{% if preview_rows %}
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-table"></i> Vorschau (erste {{ preview_rows|length }} Zeilen)
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
{% for col in header_previews %}
|
||||
<th><small>{{ col.header }}</small></th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in preview_rows %}
|
||||
<tr>
|
||||
{% for cell in row %}
|
||||
<td><small>{{ cell|truncatechars:40 }}</small></td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Import Mode -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-cog"></i> Import-Modus
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="import_mode" id="mode_skip" value="skip" checked>
|
||||
<label class="form-check-label" for="mode_skip">
|
||||
<strong>Nur neue importieren</strong>
|
||||
<br><small class="text-muted">Bereits vorhandene Einträge werden übersprungen</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="import_mode" id="mode_merge" value="merge">
|
||||
<label class="form-check-label" for="mode_merge">
|
||||
<strong>Zusammenführen</strong>
|
||||
<br><small class="text-muted">Vorhandene Einträge werden mit neuen Daten aktualisiert</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="import_mode" id="mode_create" value="create">
|
||||
<label class="form-check-label" for="mode_create">
|
||||
<strong>Alle neu anlegen</strong>
|
||||
<br><small class="text-muted">Keine Duplikatprüfung, alle Zeilen als neue Einträge</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'stiftung:import_export_hub' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times"></i> Abbrechen
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-upload"></i> {{ total_rows }} Zeilen importieren
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script>
|
||||
// Auto-apply mapping from server
|
||||
const autoMapping = {{ auto_mapping_json|safe }};
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
for (const [colIdx, fieldName] of Object.entries(autoMapping)) {
|
||||
const select = document.querySelector(`select[name="mapping_${colIdx}"]`);
|
||||
if (select) {
|
||||
for (const opt of select.options) {
|
||||
if (opt.value === fieldName) {
|
||||
opt.selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight duplicate mappings
|
||||
const selects = document.querySelectorAll('.mapping-select');
|
||||
selects.forEach(sel => {
|
||||
sel.addEventListener('change', highlightDuplicates);
|
||||
});
|
||||
highlightDuplicates();
|
||||
});
|
||||
|
||||
function highlightDuplicates() {
|
||||
const selects = document.querySelectorAll('.mapping-select');
|
||||
const valueCount = {};
|
||||
|
||||
selects.forEach(sel => {
|
||||
const val = sel.value;
|
||||
if (val && val !== '__skip__') {
|
||||
valueCount[val] = (valueCount[val] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
selects.forEach(sel => {
|
||||
const val = sel.value;
|
||||
if (val && val !== '__skip__' && valueCount[val] > 1) {
|
||||
sel.classList.add('is-invalid');
|
||||
} else {
|
||||
sel.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user