Files
2026-09-09 16:55:08 +02:00

255 lines
7.7 KiB
Vue

<script setup>
import {computed, ref, watch} from 'vue'
/**
* Formular für das CSV-Format eines Kontoauszugs (Options-Typ `bank-ruleset`).
*
* Zwei Zustände: „App-Standard" (v-model leer) und „eigenes Format" (v-model trägt ein JSON-Objekt).
* Das ist die Zusage aus dem Backend -- ein Override gilt ganz oder gar nicht, es wird nicht feldweise
* mit dem Standard gemischt. Deshalb schaltet hier ein Schalter um und nicht jedes Feld für sich.
*
* Der Wert geht als JSON-String durch das generische Options-Formular; das Zahlungsmodul dekodiert ihn.
*/
const props = defineProps({
// JSON-String oder Objekt; leer = App-Standard.
modelValue: {type: [String, Object], default: ''},
// Der app-weite Standard, als Vorbelegung und Vergleich.
defaults: {type: Object, default: () => ({})},
charsets: {type: Array, default: () => ['auto', 'UTF-8', 'Windows-1252', 'ISO-8859-15']},
})
const emit = defineEmits(['update:modelValue'])
const FIELDS = [
{key: 'payment_date', label: 'Zahlungsdatum', required: true, hint: 'Üblich: der Buchungstag.'},
{key: 'purpose', label: 'Verwendungszweck', required: true},
{key: 'amount', label: 'Betrag', required: true},
{key: 'payer_name', label: 'Name des Zahlers', required: false},
{key: 'payer_iban', label: 'IBAN des Zahlers', required: false, hint: 'Ohne diese Spalte bleiben die Erstattungsdaten leer.'},
]
function parse(value) {
if (!value) return null
if (typeof value === 'object') return value
try {
const parsed = JSON.parse(value)
return parsed && typeof parsed === 'object' ? parsed : null
} catch {
return null
}
}
function blank() {
return {
delimiter: ';',
enclosure: '',
charset: 'auto',
has_header: true,
date_format: 'd.m.Y',
decimal_separator: ',',
thousands_separator: '.',
columns: {},
}
}
// Eine tiefe Kopie, damit das Bearbeiten nicht die angezeigten Standardwerte überschreibt.
function fromDefaults() {
return JSON.parse(JSON.stringify({...blank(), ...props.defaults}))
}
const custom = ref(parse(props.modelValue) !== null)
const form = ref(parse(props.modelValue) ?? fromDefaults())
watch(() => props.modelValue, (value) => {
const parsed = parse(value)
custom.value = parsed !== null
form.value = parsed ?? fromDefaults()
})
const shown = computed(() => (custom.value ? form.value : fromDefaults()))
const missingRequired = computed(
() => FIELDS.filter(field => field.required && !(shown.value.columns ?? {})[field.key]).map(field => field.label),
)
function useAppDefault() {
custom.value = false
emit('update:modelValue', '')
}
function useOwnFormat() {
custom.value = true
form.value = fromDefaults()
push()
}
function push() {
emit('update:modelValue', JSON.stringify(form.value))
}
function setColumn(key, value) {
form.value.columns = {...(form.value.columns ?? {})}
if (value) {
form.value.columns[key] = value
} else {
delete form.value.columns[key]
}
push()
}
</script>
<template>
<div class="ruleset">
<label class="mode">
<input type="radio" :checked="!custom" @change="useAppDefault"/>
App-Standard verwenden
</label>
<label class="mode">
<input type="radio" :checked="custom" @change="useOwnFormat"/>
Eigenes Format für unsere Bank
</label>
<p v-if="!custom" class="note">
Es gilt der app-weite Standard. Bearbeitet ihr das Format, gilt ausschließlich eure
Einstellung der Standard wirkt dann nicht mehr nach.
</p>
<table class="ruleset-table">
<tr>
<td>Trennzeichen</td>
<td>
<input type="text" class="form-input short" :disabled="!custom"
:value="shown.delimiter" @input="form.delimiter = $event.target.value; push()"/>
</td>
</tr>
<tr>
<td>Anführungszeichen</td>
<td>
<input type="text" class="form-input short" :disabled="!custom" placeholder="(keine)"
:value="shown.enclosure" @input="form.enclosure = $event.target.value; push()"/>
</td>
</tr>
<tr>
<td>Zeichensatz</td>
<td>
<select class="form-input" :disabled="!custom"
:value="shown.charset" @change="form.charset = $event.target.value; push()">
<option v-for="charset in charsets" :key="charset" :value="charset">
{{ charset === 'auto' ? 'Automatisch erkennen' : charset }}
</option>
</select>
</td>
</tr>
<tr>
<td>Datumsformat</td>
<td>
<input type="text" class="form-input short" :disabled="!custom"
:value="shown.date_format" @input="form.date_format = $event.target.value; push()"/>
<span class="option-hint">PHP-Schreibweise, z.&nbsp;B. d.m.Y für 09.09.2026</span>
</td>
</tr>
<tr>
<td>Dezimaltrenner</td>
<td>
<input type="text" class="form-input short" :disabled="!custom"
:value="shown.decimal_separator" @input="form.decimal_separator = $event.target.value; push()"/>
</td>
</tr>
<tr>
<td>Tausendertrenner</td>
<td>
<input type="text" class="form-input short" :disabled="!custom" placeholder="(keiner)"
:value="shown.thousands_separator" @input="form.thousands_separator = $event.target.value; push()"/>
</td>
</tr>
</table>
<h4>Spalten der Datei</h4>
<p class="note">
Die Überschrift der jeweiligen Spalte, genau so wie sie in der ersten Zeile des Exports steht.
</p>
<table class="ruleset-table">
<tr v-for="field in FIELDS" :key="field.key">
<td>{{ field.label }}<span v-if="field.required" class="required-marker">*</span></td>
<td>
<input type="text" class="form-input" :disabled="!custom"
:value="(shown.columns ?? {})[field.key] ?? ''"
@input="setColumn(field.key, $event.target.value)"/>
<span v-if="field.hint" class="option-hint">{{ field.hint }}</span>
</td>
</tr>
</table>
<p v-if="custom && missingRequired.length" class="warning">
Ohne {{ missingRequired.join(', ') }} lässt sich kein Kontoauszug einlesen.
</p>
</div>
</template>
<style scoped>
.ruleset {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px 14px;
background-color: #fafafa;
}
.mode {
display: block;
margin-bottom: 4px;
cursor: pointer;
}
.mode input {
margin-right: 6px;
}
.note {
margin: 6px 0 10px 0;
color: #6b7280;
font-size: 0.85rem;
}
.ruleset-table {
width: 100%;
border-collapse: collapse;
}
.ruleset-table td {
padding: 4px 6px 4px 0;
vertical-align: top;
}
.ruleset-table td:first-child {
width: 190px;
color: #374151;
}
.form-input.short {
width: 90px;
}
h4 {
margin: 16px 0 0 0;
color: #374151;
}
.option-hint {
display: block;
color: #6b7280;
font-size: 0.8rem;
margin-top: 2px;
}
.required-marker {
color: #ef4444;
margin-left: 2px;
}
.warning {
margin: 10px 0 0 0;
color: #991b1b;
font-size: 0.85rem;
}
</style>