255 lines
7.7 KiB
Vue
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. 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>
|