Passt Kassenbuch Ansicht an
Kassenbücher werden nun zuerst tabellarisch angezeigt. Das Erstellen einer Barkasse erfolgt über ein Modal und einzelne Kassenbücher öffnen sich über eine Detailseite.
This commit is contained in:
428
frontend/pages/accounting/cashbooks/[id].vue
Normal file
428
frontend/pages/accounting/cashbooks/[id].vue
Normal file
@@ -0,0 +1,428 @@
|
||||
<script setup>
|
||||
import dayjs from "dayjs"
|
||||
|
||||
const toast = useToast()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const savingBooking = ref(false)
|
||||
const cashbooks = ref([])
|
||||
const bookings = ref([])
|
||||
const accounts = ref([])
|
||||
const customers = ref([])
|
||||
const vendors = ref([])
|
||||
const ownaccounts = ref([])
|
||||
const incomingInvoices = ref([])
|
||||
const selectedCashbookId = computed(() => Number(route.params.id))
|
||||
const counterSearch = ref("")
|
||||
const expandedGroups = ref([])
|
||||
|
||||
const DATEV_TAX_KEY_ITEMS = [
|
||||
{ value: "__none__", label: "Ohne Steuerschlüssel" },
|
||||
{ value: "9", label: "9 - Vorsteuer 19 %" },
|
||||
{ value: "8", label: "8 - Vorsteuer 7 %" },
|
||||
{ value: "19", label: "19 - EU Vorsteuer 19 %" },
|
||||
{ value: "18", label: "18 - EU Vorsteuer 7 %" }
|
||||
]
|
||||
|
||||
const form = reactive({
|
||||
date: dayjs().format("YYYY-MM-DD"),
|
||||
direction: "expense",
|
||||
amount: null,
|
||||
counter: "",
|
||||
datevTaxKey: "__none__",
|
||||
description: ""
|
||||
})
|
||||
|
||||
const displayCurrency = (value) => `${Number(value || 0).toFixed(2).replace(".", ",")} €`
|
||||
const normalizeSearch = (value) => String(value || "").toLowerCase().trim()
|
||||
|
||||
const selectedCashbook = computed(() => cashbooks.value.find((item) => item.id === selectedCashbookId.value) || null)
|
||||
const currentBalance = computed(() => {
|
||||
const openingBalance = Number(selectedCashbook.value?.balance || 0)
|
||||
const movementSum = bookings.value.reduce((sum, booking) => sum + Number(booking.amount || 0), 0)
|
||||
return openingBalance + movementSum
|
||||
})
|
||||
|
||||
const getIncomingInvoiceGross = (invoice) => {
|
||||
return Number((invoice.accounts || []).reduce((sum, account) => {
|
||||
return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0)
|
||||
}, 0))
|
||||
}
|
||||
|
||||
const getIncomingInvoiceOpenAmount = (invoice) => {
|
||||
const gross = getIncomingInvoiceGross(invoice)
|
||||
const allocated = Number((invoice.statementallocations || []).reduce((sum, allocation) => sum + Number(allocation.amount || 0), 0))
|
||||
return Math.abs(gross) - Math.abs(allocated)
|
||||
}
|
||||
|
||||
const buildEntries = (rows, type, labelBuilder) =>
|
||||
(rows || []).map((item) => ({
|
||||
key: `${type}:${item.id}`,
|
||||
id: item.id,
|
||||
type,
|
||||
number: item.number || item.vendorNumber || item.customerNumber || item.reference || "",
|
||||
name: item.label || item.name || item.vendor?.name || "",
|
||||
label: labelBuilder(item),
|
||||
typeLabel:
|
||||
type === "account"
|
||||
? "Sachkonten"
|
||||
: type === "vendor"
|
||||
? "Kreditoren"
|
||||
: type === "customer"
|
||||
? "Debitoren"
|
||||
: type === "incominginvoice"
|
||||
? "Eingangsbelege"
|
||||
: "Zusätzliche Konten"
|
||||
}))
|
||||
|
||||
const entryGroups = computed(() => ([
|
||||
{
|
||||
key: "account",
|
||||
label: "Sachkonten",
|
||||
entries: buildEntries(accounts.value, "account", (item) => `${item.number} - ${item.label}`)
|
||||
},
|
||||
{
|
||||
key: "vendor",
|
||||
label: "Kreditoren",
|
||||
entries: buildEntries(vendors.value, "vendor", (item) => `${item.vendorNumber || "ohne Nr."} - ${item.name}`)
|
||||
},
|
||||
{
|
||||
key: "customer",
|
||||
label: "Debitoren",
|
||||
entries: buildEntries(customers.value, "customer", (item) => `${item.customerNumber || "ohne Nr."} - ${item.name}`)
|
||||
},
|
||||
{
|
||||
key: "ownaccount",
|
||||
label: "Zusätzliche Konten",
|
||||
entries: buildEntries(ownaccounts.value, "ownaccount", (item) => `${item.number} - ${item.name}`)
|
||||
},
|
||||
{
|
||||
key: "incominginvoice",
|
||||
label: "Eingangsbelege",
|
||||
entries: buildEntries(incomingInvoices.value, "incominginvoice", (item) => `${item.reference || "Ohne Referenz"} - ${item.vendor?.name || "Ohne Lieferant"} - Offen ${displayCurrency(getIncomingInvoiceOpenAmount(item))}`)
|
||||
}
|
||||
]))
|
||||
|
||||
const groupedCounterEntries = computed(() =>
|
||||
entryGroups.value.map((group) => ({
|
||||
...group,
|
||||
entries: group.entries.filter((entry) => {
|
||||
const search = normalizeSearch(counterSearch.value)
|
||||
if (!search) return true
|
||||
return [entry.number, entry.name, entry.label, entry.typeLabel]
|
||||
.some((value) => normalizeSearch(value).includes(search))
|
||||
})
|
||||
})).filter((group) => group.entries.length > 0)
|
||||
)
|
||||
|
||||
const selectedCounter = computed(() => entryGroups.value.flatMap((group) => group.entries).find((entry) => entry.key === form.counter))
|
||||
|
||||
const isGroupExpanded = (groupKey) => expandedGroups.value.includes(groupKey)
|
||||
const toggleGroupExpanded = (groupKey) => {
|
||||
if (expandedGroups.value.includes(groupKey)) {
|
||||
expandedGroups.value = expandedGroups.value.filter((item) => item !== groupKey)
|
||||
return
|
||||
}
|
||||
expandedGroups.value = [...expandedGroups.value, groupKey]
|
||||
}
|
||||
|
||||
const visibleEntries = (group) => {
|
||||
if (group.entries.length <= 5 || isGroupExpanded(group.key)) return group.entries
|
||||
return group.entries.slice(0, 5)
|
||||
}
|
||||
|
||||
const getBookingCounter = (booking) => {
|
||||
if (booking.incominginvoice) {
|
||||
return {
|
||||
type: "Eingangsbeleg",
|
||||
number: booking.incominginvoice.reference || "",
|
||||
name: booking.incominginvoice.vendor?.name || booking.incominginvoice.description || ""
|
||||
}
|
||||
}
|
||||
|
||||
const map = [
|
||||
["account", "Sachkonto"],
|
||||
["vendor", "Kreditor"],
|
||||
["customer", "Debitor"],
|
||||
["ownaccount", "Zusätzliches Konto"]
|
||||
]
|
||||
|
||||
for (const [key, type] of map) {
|
||||
const item = booking[key]
|
||||
if (!item) continue
|
||||
return {
|
||||
type,
|
||||
number: item.number || item.vendorNumber || item.customerNumber || "",
|
||||
name: item.label || item.name || ""
|
||||
}
|
||||
}
|
||||
|
||||
return { type: "", number: "", name: "" }
|
||||
}
|
||||
|
||||
const loadBookings = async () => {
|
||||
if (!selectedCashbookId.value || !Number.isFinite(selectedCashbookId.value)) {
|
||||
bookings.value = []
|
||||
return
|
||||
}
|
||||
|
||||
bookings.value = await useNuxtApp().$api(`/api/banking/cashbooks/${selectedCashbookId.value}/bookings`)
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
const [cashbookRows, accountRows, customerRows, vendorRows, ownaccountRows, incomingInvoiceRows] = await Promise.all([
|
||||
useNuxtApp().$api("/api/banking/cashbooks"),
|
||||
useEntities("accounts").selectSpecial("*", "number", true),
|
||||
useEntities("customers").select(),
|
||||
useEntities("vendors").select(),
|
||||
useEntities("ownaccounts").select(),
|
||||
useEntities("incominginvoices").select("*, vendor(*), statementallocations(id,amount)")
|
||||
])
|
||||
|
||||
cashbooks.value = cashbookRows || []
|
||||
accounts.value = accountRows || []
|
||||
customers.value = customerRows || []
|
||||
vendors.value = vendorRows || []
|
||||
ownaccounts.value = ownaccountRows || []
|
||||
incomingInvoices.value = (incomingInvoiceRows || [])
|
||||
.filter((invoice) => invoice.state === "Gebucht" && !invoice.archived)
|
||||
.filter((invoice) => getIncomingInvoiceOpenAmount(invoice) > 0.004)
|
||||
|
||||
await loadBookings()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const saveBooking = async () => {
|
||||
if (!selectedCashbookId.value || !form.date || !form.amount || !form.counter) {
|
||||
toast.add({ title: "Bitte Kasse, Datum, Betrag und Gegenkonto auswählen.", color: "warning" })
|
||||
return
|
||||
}
|
||||
|
||||
const [counterType, counterId] = String(form.counter).split(":")
|
||||
savingBooking.value = true
|
||||
try {
|
||||
await useNuxtApp().$api(`/api/banking/cashbooks/${selectedCashbookId.value}/bookings`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
date: form.date,
|
||||
direction: form.direction,
|
||||
amount: Number(form.amount),
|
||||
counterType,
|
||||
counterId,
|
||||
datevTaxKey: form.datevTaxKey === "__none__" ? null : form.datevTaxKey,
|
||||
description: form.description
|
||||
}
|
||||
})
|
||||
toast.add({ title: "Kassenbuchung erstellt." })
|
||||
form.amount = null
|
||||
form.counter = ""
|
||||
form.datevTaxKey = "__none__"
|
||||
form.description = ""
|
||||
counterSearch.value = ""
|
||||
expandedGroups.value = []
|
||||
await loadBookings()
|
||||
} finally {
|
||||
savingBooking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteBooking = async (booking) => {
|
||||
await useNuxtApp().$api(`/api/banking/cashbook-bookings/${booking.id}`, { method: "DELETE" })
|
||||
toast.add({ title: "Kassenbuchung gelöscht." })
|
||||
await loadBookings()
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanelContent>
|
||||
<UDashboardNavbar :title="selectedCashbook ? selectedCashbook.name : 'Kassenbuch'">
|
||||
<template #left>
|
||||
<UButton icon="i-heroicons-arrow-left" variant="ghost" color="neutral" @click="router.push('/accounting/cashbooks')">
|
||||
Kassenbücher
|
||||
</UButton>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<div v-if="loading" class="py-10 text-center text-gray-500">Lade Kassenbuch...</div>
|
||||
<div v-else-if="selectedCashbook" class="space-y-6">
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500">Aktuelle Kasse</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900 dark:text-white">{{ selectedCashbook.name }}</div>
|
||||
</UCard>
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500">Kontennummer</div>
|
||||
<div class="mt-1 font-mono text-lg font-semibold text-gray-900 dark:text-white">{{ selectedCashbook.datevNumber }}</div>
|
||||
</UCard>
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500">Kassenbestand</div>
|
||||
<div class="mt-1 text-lg font-semibold" :class="currentBalance < 0 ? 'text-red-600' : 'text-emerald-600'">
|
||||
{{ displayCurrency(currentBalance) }}
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<h2 class="font-semibold text-gray-900 dark:text-white">Neue Kassenbuchung</h2>
|
||||
<p class="text-sm text-gray-500">Einnahmen erhöhen den Kassenbestand, Ausgaben verringern ihn.</p>
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<UFormField label="Buchungsdatum">
|
||||
<UInput v-model="form.date" type="date" />
|
||||
</UFormField>
|
||||
<UFormField label="Betrag">
|
||||
<UInput v-model="form.amount" type="number" min="0" step="0.01" placeholder="0,00" />
|
||||
</UFormField>
|
||||
<UFormField label="DATEV-Steuerschlüssel">
|
||||
<USelect
|
||||
v-model="form.datevTaxKey"
|
||||
:items="DATEV_TAX_KEY_ITEMS"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[240px_minmax(0,1fr)]">
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg border px-4 py-3 text-left transition"
|
||||
:class="form.direction === 'income'
|
||||
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-950/30'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300 dark:border-gray-800 dark:bg-gray-900'"
|
||||
@click="form.direction = 'income'"
|
||||
>
|
||||
<div class="font-semibold text-emerald-700 dark:text-emerald-300">Einnahme</div>
|
||||
<div class="text-sm text-gray-500">Bargeld kommt in die Kasse.</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg border px-4 py-3 text-left transition"
|
||||
:class="form.direction === 'expense'
|
||||
? 'border-red-500 bg-red-50 dark:bg-red-950/30'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300 dark:border-gray-800 dark:bg-gray-900'"
|
||||
@click="form.direction = 'expense'"
|
||||
>
|
||||
<div class="font-semibold text-red-700 dark:text-red-300">Ausgabe</div>
|
||||
<div class="text-sm text-gray-500">Bargeld wird aus der Kasse entnommen.</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Gegenkonto</h3>
|
||||
<p class="text-sm text-gray-500">Sachkonto, Kreditor, Debitor, Eingangsbeleg oder zusätzliches Konto.</p>
|
||||
</div>
|
||||
<UInput v-model="counterSearch" icon="i-heroicons-magnifying-glass" placeholder="Gegenkonto durchsuchen..." class="max-w-sm" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-for="group in groupedCounterEntries" :key="group.key" class="space-y-2">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500">{{ group.label }}</div>
|
||||
<div class="grid gap-2 md:grid-cols-2">
|
||||
<button
|
||||
v-for="entry in visibleEntries(group)"
|
||||
:key="entry.key"
|
||||
type="button"
|
||||
class="rounded-lg border px-3 py-2 text-left transition"
|
||||
:class="form.counter === entry.key
|
||||
? 'border-primary-500 bg-primary-50 dark:bg-primary-950/30'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300 dark:border-gray-800 dark:bg-gray-900'"
|
||||
@click="form.counter = entry.key"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-mono text-sm">{{ entry.number }}</span>
|
||||
<span class="text-sm">{{ entry.name }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<UButton
|
||||
v-if="group.entries.length > 5"
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
:label="isGroupExpanded(group.key) ? 'Weniger anzeigen' : `${group.entries.length - 5} weitere anzeigen`"
|
||||
@click="toggleGroupExpanded(group.key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="selectedCounter"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
icon="i-heroicons-arrows-right-left"
|
||||
:title="form.direction === 'income' ? `${selectedCashbook.datevNumber} an ${selectedCounter.number}` : `${selectedCounter.number} an ${selectedCashbook.datevNumber}`"
|
||||
:description="`${selectedCounter.typeLabel.slice(0, -1)} ${selectedCounter.name} wird als Gegenkonto verwendet.`"
|
||||
/>
|
||||
|
||||
<UFormField label="Beschreibung">
|
||||
<UTextarea v-model="form.description" placeholder="z. B. Bareinkauf Büromaterial" autoresize class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<UButton color="primary" icon="i-heroicons-check" :loading="savingBooking" @click="saveBooking">
|
||||
Kassenbuchung erstellen
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div>
|
||||
<h2 class="font-semibold text-gray-900 dark:text-white">Kassenbewegungen</h2>
|
||||
<p class="text-sm text-gray-500">Alle Buchungen dieser Barkasse mit Gegenkonto.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="bookings.length === 0" class="py-10 text-center text-gray-500">Noch keine Kassenbuchungen erfasst.</div>
|
||||
<div v-else class="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
<div v-for="booking in bookings" :key="booking.id" class="py-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm">
|
||||
<span class="font-medium">{{ dayjs(booking.date).format("DD.MM.YYYY") }}</span>
|
||||
<UBadge :color="Number(booking.amount) >= 0 ? 'success' : 'error'" variant="subtle">
|
||||
{{ Number(booking.amount) >= 0 ? "Einnahme" : "Ausgabe" }}
|
||||
</UBadge>
|
||||
<span class="font-mono font-semibold">{{ displayCurrency(Math.abs(Number(booking.amount || 0))) }}</span>
|
||||
<span class="text-gray-500">{{ getBookingCounter(booking).type }}</span>
|
||||
<span class="font-mono">{{ getBookingCounter(booking).number }}</span>
|
||||
<span>{{ getBookingCounter(booking).name }}</span>
|
||||
</div>
|
||||
<UButton
|
||||
icon="i-heroicons-trash"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="deleteBooking(booking)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="booking.text" class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ booking.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
</div>
|
||||
|
||||
<UCard v-else>
|
||||
<div class="py-16 text-center text-gray-500">Dieses Kassenbuch wurde nicht gefunden.</div>
|
||||
</UCard>
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
165
frontend/pages/accounting/cashbooks/index.vue
Normal file
165
frontend/pages/accounting/cashbooks/index.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<script setup>
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
const tempStore = useTempStore()
|
||||
|
||||
const loading = ref(true)
|
||||
const savingCashbook = ref(false)
|
||||
const createCashbookModalOpen = ref(false)
|
||||
const cashbooks = ref([])
|
||||
const searchString = ref(tempStore.searchStrings["cashbooks"] || "")
|
||||
|
||||
const newCashbook = reactive({
|
||||
name: "",
|
||||
datevNumber: "1000",
|
||||
openingBalance: 0
|
||||
})
|
||||
|
||||
const displayCurrency = (value) => `${Number(value || 0).toFixed(2).replace(".", ",")} €`
|
||||
|
||||
const templateColumns = [
|
||||
{
|
||||
key: "name",
|
||||
label: "Bezeichnung"
|
||||
},
|
||||
{
|
||||
key: "datevNumber",
|
||||
label: "Kontennummer"
|
||||
},
|
||||
{
|
||||
key: "balance",
|
||||
label: "Anfangsbestand"
|
||||
},
|
||||
{
|
||||
key: "syncedAt",
|
||||
label: "Erstellt"
|
||||
}
|
||||
]
|
||||
|
||||
const filteredRows = computed(() => useSearch(searchString.value, cashbooks.value))
|
||||
|
||||
const clearSearchString = () => {
|
||||
tempStore.clearSearchString("cashbooks")
|
||||
searchString.value = ""
|
||||
}
|
||||
|
||||
const loadCashbooks = async () => {
|
||||
loading.value = true
|
||||
cashbooks.value = await useNuxtApp().$api("/api/banking/cashbooks")
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const resetCreateForm = () => {
|
||||
newCashbook.name = ""
|
||||
newCashbook.datevNumber = "1000"
|
||||
newCashbook.openingBalance = 0
|
||||
}
|
||||
|
||||
const createCashbook = async () => {
|
||||
if (!newCashbook.name || !newCashbook.datevNumber) {
|
||||
toast.add({ title: "Bitte Bezeichnung und Kontennummer ausfüllen.", color: "warning" })
|
||||
return
|
||||
}
|
||||
|
||||
savingCashbook.value = true
|
||||
try {
|
||||
const created = await useNuxtApp().$api("/api/banking/cashbooks", {
|
||||
method: "POST",
|
||||
body: {
|
||||
name: newCashbook.name,
|
||||
datevNumber: newCashbook.datevNumber,
|
||||
openingBalance: Number(newCashbook.openingBalance || 0)
|
||||
}
|
||||
})
|
||||
toast.add({ title: "Barkasse erstellt." })
|
||||
createCashbookModalOpen.value = false
|
||||
resetCreateForm()
|
||||
await loadCashbooks()
|
||||
router.push(`/accounting/cashbooks/${created.id}`)
|
||||
} finally {
|
||||
savingCashbook.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadCashbooks)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanelContent>
|
||||
<UDashboardNavbar title="Kassenbücher" :badge="filteredRows.length">
|
||||
<template #right>
|
||||
<UInput
|
||||
id="searchinput"
|
||||
v-model="searchString"
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
autocomplete="off"
|
||||
placeholder="Suche..."
|
||||
class="hidden lg:block"
|
||||
@keydown.esc="$event.target.blur()"
|
||||
@change="tempStore.modifySearchString('cashbooks', searchString)"
|
||||
/>
|
||||
<UButton
|
||||
v-if="searchString.length > 0"
|
||||
icon="i-heroicons-x-mark"
|
||||
variant="outline"
|
||||
color="error"
|
||||
@click="clearSearchString"
|
||||
/>
|
||||
<UButton icon="i-heroicons-plus" @click="createCashbookModalOpen = true">
|
||||
Barkasse
|
||||
</UButton>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UTable
|
||||
:data="filteredRows"
|
||||
:columns="normalizeTableColumns(templateColumns)"
|
||||
:loading="loading"
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
:on-select="(row) => router.push(`/accounting/cashbooks/${row.original?.id || row.id}`)"
|
||||
:empty="{ icon: 'i-heroicons-banknotes', label: 'Keine Kassenbücher angelegt' }"
|
||||
>
|
||||
<template #datevNumber-cell="{ row }">
|
||||
<span class="font-mono">{{ row.original.datevNumber }}</span>
|
||||
</template>
|
||||
<template #balance-cell="{ row }">
|
||||
{{ displayCurrency(row.original.balance) }}
|
||||
</template>
|
||||
<template #syncedAt-cell="{ row }">
|
||||
{{ row.original.createdAt ? new Date(row.original.createdAt).toLocaleDateString("de-DE") : "-" }}
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<UModal v-model:open="createCashbookModalOpen">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="text-lg font-semibold">Barkasse anlegen</div>
|
||||
</template>
|
||||
|
||||
<UForm :state="newCashbook" class="space-y-4" @submit.prevent="createCashbook">
|
||||
<UFormField label="Bezeichnung">
|
||||
<UInput v-model="newCashbook.name" placeholder="z. B. Hauptkasse" />
|
||||
</UFormField>
|
||||
<UFormField label="Kontennummer">
|
||||
<UInput v-model="newCashbook.datevNumber" placeholder="1000" />
|
||||
</UFormField>
|
||||
<UFormField label="Anfangsbestand">
|
||||
<UInput v-model="newCashbook.openingBalance" type="number" step="0.01" />
|
||||
</UFormField>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<UButton color="gray" variant="soft" @click="createCashbookModalOpen = false">
|
||||
Abbrechen
|
||||
</UButton>
|
||||
<UButton type="submit" color="primary" :loading="savingCashbook">
|
||||
Barkasse anlegen
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
Reference in New Issue
Block a user