Added IBAN Saving, Automatic Saving, added Mitglieder
This commit is contained in:
148
frontend/components/BankAccountAssignInput.vue
Normal file
148
frontend/components/BankAccountAssignInput.vue
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue"])
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const accounts = ref([])
|
||||||
|
const ibanSearch = ref("")
|
||||||
|
const showCreate = ref(false)
|
||||||
|
|
||||||
|
const createPayload = ref({
|
||||||
|
iban: "",
|
||||||
|
bic: "",
|
||||||
|
bankName: "",
|
||||||
|
description: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeIban = (value) => String(value || "").replace(/\s+/g, "").toUpperCase()
|
||||||
|
|
||||||
|
const loadAccounts = async () => {
|
||||||
|
accounts.value = await useEntities("entitybankaccounts").select()
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignedIds = computed(() => {
|
||||||
|
return Array.isArray(props.modelValue) ? props.modelValue : []
|
||||||
|
})
|
||||||
|
|
||||||
|
const assignedAccounts = computed(() => {
|
||||||
|
return accounts.value.filter((a) => assignedIds.value.includes(a.id))
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateAssigned = (ids) => {
|
||||||
|
emit("update:modelValue", ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignByIban = async () => {
|
||||||
|
const search = normalizeIban(ibanSearch.value)
|
||||||
|
if (!search) return
|
||||||
|
|
||||||
|
const match = accounts.value.find((a) => normalizeIban(a.iban) === search)
|
||||||
|
if (!match) {
|
||||||
|
toast.add({ title: "Kein Bankkonto mit dieser IBAN gefunden.", color: "rose" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assignedIds.value.includes(match.id)) {
|
||||||
|
toast.add({ title: "Dieses Bankkonto ist bereits zugewiesen.", color: "amber" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAssigned([...assignedIds.value, match.id])
|
||||||
|
ibanSearch.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAssigned = (id) => {
|
||||||
|
updateAssigned(assignedIds.value.filter((i) => i !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAndAssign = async () => {
|
||||||
|
if (!createPayload.value.iban || !createPayload.value.bic || !createPayload.value.bankName) {
|
||||||
|
toast.add({ title: "IBAN, BIC und Bankinstitut sind Pflichtfelder.", color: "rose" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await useEntities("entitybankaccounts").create(createPayload.value, true)
|
||||||
|
await loadAccounts()
|
||||||
|
updateAssigned([...assignedIds.value, created.id])
|
||||||
|
createPayload.value = { iban: "", bic: "", bankName: "", description: "" }
|
||||||
|
showCreate.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAccounts()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 w-full">
|
||||||
|
<div class="flex flex-wrap gap-2" v-if="assignedAccounts.length > 0">
|
||||||
|
<UBadge
|
||||||
|
v-for="account in assignedAccounts"
|
||||||
|
:key="account.id"
|
||||||
|
color="primary"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{{ account.displayLabel || account.iban }}
|
||||||
|
<UButton
|
||||||
|
v-if="!disabled"
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
size="2xs"
|
||||||
|
icon="i-heroicons-x-mark"
|
||||||
|
class="ml-1"
|
||||||
|
@click="removeAssigned(account.id)"
|
||||||
|
/>
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InputGroup class="w-full">
|
||||||
|
<UInput
|
||||||
|
v-model="ibanSearch"
|
||||||
|
class="flex-auto"
|
||||||
|
placeholder="IBAN eingeben und zuweisen"
|
||||||
|
:disabled="disabled"
|
||||||
|
@keydown.enter.prevent="assignByIban"
|
||||||
|
/>
|
||||||
|
<UButton :disabled="disabled" @click="assignByIban">
|
||||||
|
Zuweisen
|
||||||
|
</UButton>
|
||||||
|
<UButton :disabled="disabled" color="gray" variant="outline" @click="showCreate = true">
|
||||||
|
Neu
|
||||||
|
</UButton>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UModal v-model="showCreate">
|
||||||
|
<UCard>
|
||||||
|
<template #header>Neue Bankverbindung erstellen</template>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<UFormGroup label="IBAN">
|
||||||
|
<UInput v-model="createPayload.iban" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="BIC">
|
||||||
|
<UInput v-model="createPayload.bic" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Bankinstitut">
|
||||||
|
<UInput v-model="createPayload.bankName" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Beschreibung (optional)">
|
||||||
|
<UInput v-model="createPayload.description" />
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<UButton color="gray" variant="outline" @click="showCreate = false">Abbrechen</UButton>
|
||||||
|
<UButton @click="createAndAssign">Erstellen und zuweisen</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
65
frontend/components/columnRenderings/bankAccounts.vue
Normal file
65
frontend/components/columnRenderings/bankAccounts.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
row: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const accounts = ref([])
|
||||||
|
const showFullIban = ref(false)
|
||||||
|
|
||||||
|
const formatIban = (iban) => {
|
||||||
|
const cleaned = String(iban || "").replace(/\s+/g, "").toUpperCase()
|
||||||
|
if (!cleaned) return ""
|
||||||
|
return cleaned.match(/.{1,4}/g)?.join(" ") || cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskIban = (iban) => {
|
||||||
|
const cleaned = String(iban || "").replace(/\s+/g, "").toUpperCase()
|
||||||
|
if (!cleaned) return ""
|
||||||
|
if (cleaned.length <= 8) return cleaned
|
||||||
|
return `${cleaned.slice(0, 4)} **** **** ${cleaned.slice(-4)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const bankAccounts = computed(() => {
|
||||||
|
const ids = Array.isArray(props.row?.infoData?.bankAccountIds) ? props.row.infoData.bankAccountIds : []
|
||||||
|
if (!ids.length) return []
|
||||||
|
return accounts.value
|
||||||
|
.filter((a) => ids.includes(a.id))
|
||||||
|
.map((a) => {
|
||||||
|
const iban = formatIban(a.iban)
|
||||||
|
const ibanDisplay = showFullIban.value ? iban : maskIban(a.iban)
|
||||||
|
const parts = [ibanDisplay]
|
||||||
|
if (a.bankName) parts.push(a.bankName)
|
||||||
|
if (a.description) parts.push(`(${a.description})`)
|
||||||
|
return {
|
||||||
|
id: a.id,
|
||||||
|
label: parts.filter(Boolean).join(" | ")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const setup = async () => {
|
||||||
|
accounts.value = await useEntities("entitybankaccounts").select()
|
||||||
|
}
|
||||||
|
|
||||||
|
setup()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div v-if="bankAccounts.length > 0" class="flex">
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
size="2xs"
|
||||||
|
:icon="showFullIban ? 'i-heroicons-eye-slash' : 'i-heroicons-eye'"
|
||||||
|
@click="showFullIban = !showFullIban"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span v-for="account in bankAccounts" :key="account.id">{{ account.label }}</span>
|
||||||
|
<span v-if="bankAccounts.length === 0">-</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user