2025-10-20 08:57:51 +02:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
|
2025-11-18 10:27:49 +01:00
|
|
|
|
import { computed, ref, onMounted } from 'vue'
|
|
|
|
|
|
import { type Invoice } from '@/types'
|
2025-11-18 20:46:40 +01:00
|
|
|
|
import { newInvoice } from '@/types/index.d'
|
2025-11-14 17:45:57 +01:00
|
|
|
|
import axios from 'axios'
|
|
|
|
|
|
import AppLayout from '@/layouts/AppLayout.vue'
|
2025-10-20 08:57:51 +02:00
|
|
|
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'
|
|
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
|
|
import DocumentTable from '@/components/documents/DocumentTable.vue'
|
2025-10-29 15:42:43 +01:00
|
|
|
|
import { ChevronLeft, ChevronRight, Plus, Search, Delete } from "lucide-vue-next"
|
2025-10-20 08:57:51 +02:00
|
|
|
|
import Fuse from 'fuse.js';
|
|
|
|
|
|
import { Input } from '@/components/ui/input'
|
2025-10-29 15:42:43 +01:00
|
|
|
|
import { toast } from 'vue-sonner'
|
2025-10-20 08:57:51 +02:00
|
|
|
|
import SelectSeparator from '@/components/ui/select/SelectSeparator.vue'
|
2025-11-03 08:42:24 +01:00
|
|
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
|
|
|
|
import { Kbd, KbdGroup } from '@/components/ui/kbd'
|
2025-11-10 16:08:02 +01:00
|
|
|
|
import { statusBadgeLabels } from '@/components/ui/status-badge'
|
2025-11-14 17:45:57 +01:00
|
|
|
|
import AppHeader from '@/components/AppHeader.vue'
|
2025-11-18 10:27:49 +01:00
|
|
|
|
import InvoiceDialog from '@/components/documents/InvoiceDialog.vue'
|
2025-10-20 08:57:51 +02:00
|
|
|
|
|
2025-11-18 10:27:49 +01:00
|
|
|
|
// initial invoice data from inertia
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
invoicesData: Invoice[];
|
|
|
|
|
|
}
|
|
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
|
|
const invoicesData = ref(props.invoicesData || [])
|
|
|
|
|
|
const activeInvoice = ref<Invoice | undefined>(undefined)
|
2025-10-20 08:57:51 +02:00
|
|
|
|
const selectedYearIndex = ref(0)
|
|
|
|
|
|
const detailDialogOpen = ref(false)
|
|
|
|
|
|
const searchQuery = ref('')
|
|
|
|
|
|
const searchField = ref()
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
2025-11-18 10:27:49 +01:00
|
|
|
|
// Load older invoices after initial page load
|
|
|
|
|
|
try {
|
|
|
|
|
|
const invoiceBeforeThisYearResponse = await axios.get('/api/invoices/summaryBeforeThisYear')
|
|
|
|
|
|
invoicesData.value = invoicesData.value.concat(invoiceBeforeThisYearResponse.data as Invoice[])
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Fehler beim Laden der Daten:', error)
|
|
|
|
|
|
}
|
2025-10-22 16:52:16 +02:00
|
|
|
|
|
|
|
|
|
|
let queryString = window.location.search
|
|
|
|
|
|
let params = new URLSearchParams(queryString)
|
2025-11-18 20:46:40 +01:00
|
|
|
|
if (params.get('action') == 'new') createInvoice()
|
2025-10-20 08:57:51 +02:00
|
|
|
|
|
2025-11-18 10:27:49 +01:00
|
|
|
|
searchField.value = document.getElementById('search')
|
|
|
|
|
|
})
|
2025-10-20 08:57:51 +02:00
|
|
|
|
|
|
|
|
|
|
const years = computed((): number[] => {
|
|
|
|
|
|
const allYears = invoicesData.value.map(invoice => {
|
|
|
|
|
|
const date = new Date(invoice.invoiceDate);
|
|
|
|
|
|
return date.getFullYear();
|
|
|
|
|
|
})
|
|
|
|
|
|
const uniqueYears = [...new Set(allYears.filter(year => !isNaN(year)))]
|
|
|
|
|
|
uniqueYears.sort((a, b) => b - a)
|
|
|
|
|
|
return uniqueYears
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-18 10:27:49 +01:00
|
|
|
|
// Filter / search
|
2025-10-20 08:57:51 +02:00
|
|
|
|
const fuse = computed(() => {
|
|
|
|
|
|
const options = {
|
2025-11-10 16:08:02 +01:00
|
|
|
|
keys: [
|
2025-11-18 10:27:49 +01:00
|
|
|
|
'billingData.companyName',
|
2025-11-10 16:08:02 +01:00
|
|
|
|
{
|
|
|
|
|
|
name: 'paymentStatus',
|
|
|
|
|
|
getFn: (invoice: Invoice) => statusBadgeLabels[invoice.paymentStatus] || invoice.paymentStatus
|
|
|
|
|
|
},
|
|
|
|
|
|
'title',
|
|
|
|
|
|
'nr'
|
|
|
|
|
|
],
|
|
|
|
|
|
threshold: 0.3,
|
2025-10-20 08:57:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new Fuse(invoicesData.value, options);
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const filteredInvoices = computed(() => {
|
|
|
|
|
|
if (!searchQuery.value) {
|
2025-11-18 10:27:49 +01:00
|
|
|
|
return years.value[selectedYearIndex.value]
|
|
|
|
|
|
? invoicesData.value.filter(invoice => {
|
2025-10-20 08:57:51 +02:00
|
|
|
|
const invoiceDate = new Date(invoice.invoiceDate);
|
|
|
|
|
|
return invoiceDate.getFullYear() === years.value[selectedYearIndex.value];
|
2025-11-18 10:27:49 +01:00
|
|
|
|
})
|
|
|
|
|
|
: invoicesData.value;
|
2025-10-20 08:57:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return fuse.value.search(searchQuery.value)
|
|
|
|
|
|
.map(result => result.item)
|
|
|
|
|
|
.filter(invoice => {
|
|
|
|
|
|
const invoiceDate = new Date(invoice.invoiceDate);
|
|
|
|
|
|
return !years.value[selectedYearIndex.value] || invoiceDate.getFullYear() === years.value[selectedYearIndex.value];
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-18 20:46:40 +01:00
|
|
|
|
const createInvoice = () => {
|
|
|
|
|
|
editInvoice(newInvoice())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const editInvoice = (invoice: Invoice) => {
|
2025-10-20 08:57:51 +02:00
|
|
|
|
// make a deep copy, so the changes in the dialog won’t affect the data until saved
|
2025-11-18 20:46:40 +01:00
|
|
|
|
activeInvoice.value = JSON.parse(JSON.stringify(invoice))
|
2025-10-20 08:57:51 +02:00
|
|
|
|
detailDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-18 20:46:40 +01:00
|
|
|
|
const onSaveInvoice = async (updatedInvoice: Invoice) => {
|
|
|
|
|
|
// Update selectedYearIndex to ensure the new invoice is displayed
|
|
|
|
|
|
const newInvoiceYear = new Date(updatedInvoice.invoiceDate).getFullYear();
|
|
|
|
|
|
const currentYearIndex = years.value.findIndex(year => year === newInvoiceYear);
|
|
|
|
|
|
if (currentYearIndex !== -1) {
|
|
|
|
|
|
selectedYearIndex.value = currentYearIndex;
|
|
|
|
|
|
}
|
2025-10-20 08:57:51 +02:00
|
|
|
|
|
2025-11-18 20:46:40 +01:00
|
|
|
|
// Update table
|
|
|
|
|
|
const index = invoicesData.value.findIndex(inv => inv.id === updatedInvoice.id);
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
invoicesData.value[index] = updatedInvoice;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
invoicesData.value.push(updatedInvoice);
|
2025-10-20 08:57:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-18 20:46:40 +01:00
|
|
|
|
const onDeleteInvoice = async (id: number) => {
|
|
|
|
|
|
const index = invoicesData.value.findIndex(invoice => invoice.id === id)
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
invoicesData.value.splice(index, 1)
|
2025-10-20 08:57:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
|
2025-11-18 10:27:49 +01:00
|
|
|
|
|
2025-11-11 11:49:38 +01:00
|
|
|
|
<!-- Function Header -->
|
2025-11-14 17:45:57 +01:00
|
|
|
|
<AppLayout title="Rechnungen">
|
|
|
|
|
|
|
|
|
|
|
|
<AppHeader>
|
|
|
|
|
|
<!-- Year select -->
|
|
|
|
|
|
<template #left>
|
|
|
|
|
|
<Button variant="ghost" :disabled="selectedYearIndex >= (years.length - 1)"
|
|
|
|
|
|
@click="selectedYearIndex++">
|
|
|
|
|
|
<ChevronLeft />
|
|
|
|
|
|
</Button>
|
2025-11-18 10:27:49 +01:00
|
|
|
|
<Select size="sm" v-model="selectedYearIndex">
|
|
|
|
|
|
<SelectTrigger class="hover:bg-accent">
|
|
|
|
|
|
<SelectValue :placeholder="(new Date()).getFullYear().toString()"
|
|
|
|
|
|
:disabled="years.length < 1" />
|
2025-11-14 17:45:57 +01:00
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectGroup>
|
|
|
|
|
|
<SelectItem :value="null">
|
|
|
|
|
|
<SelectLabel>Alle</SelectLabel>
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectSeparator />
|
|
|
|
|
|
<SelectItem v-for="(year, index) in years" :value="index">
|
|
|
|
|
|
<SelectLabel>{{ year }}</SelectLabel>
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectGroup>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Button variant="ghost" @click="selectedYearIndex--" :disabled="selectedYearIndex <= 0">
|
|
|
|
|
|
<ChevronRight />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Search field -->
|
|
|
|
|
|
<template #middle>
|
|
|
|
|
|
<Input ref="search-field" id="search" type="text" placeholder="Filtern" class="px-8 bg-background"
|
|
|
|
|
|
v-model="searchQuery" />
|
|
|
|
|
|
<span class="absolute start-0 inset-y-0 flex items-center justify-center px-2">
|
|
|
|
|
|
<Search class="size-4 text-muted-foreground" :stroke-width="1.5" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="absolute end-0 inset-y-0 flex items-center justify-center px-0 mr-1">
|
|
|
|
|
|
<Button :size="'sm'" :variant="'ghost'" @click="searchQuery = ''; searchField.focus()">
|
|
|
|
|
|
<Delete class="size-4 text-muted-foreground" :stroke-width="1.5" />
|
2025-10-20 08:57:51 +02:00
|
|
|
|
</Button>
|
2025-11-14 17:45:57 +01:00
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
2025-10-20 08:57:51 +02:00
|
|
|
|
|
2025-11-14 17:45:57 +01:00
|
|
|
|
<!-- New button -->
|
|
|
|
|
|
<template #right>
|
2025-11-03 08:42:24 +01:00
|
|
|
|
<TooltipProvider>
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
|
<TooltipTrigger>
|
2025-11-18 20:46:40 +01:00
|
|
|
|
<Button size="sm" variant="action" @click="createInvoice">
|
2025-11-03 08:42:24 +01:00
|
|
|
|
<Plus />
|
|
|
|
|
|
Neu
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
|
<TooltipContent>
|
|
|
|
|
|
<span>Neue Rechnung anlegen</span>
|
|
|
|
|
|
<KbdGroup class="ml-2">
|
|
|
|
|
|
<Kbd class="visible-mac">⌘</Kbd>
|
|
|
|
|
|
<Kbd class="visible-pc">Ctrl</Kbd>
|
|
|
|
|
|
<Kbd>N</Kbd>
|
|
|
|
|
|
</KbdGroup>
|
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</TooltipProvider>
|
2025-11-14 17:45:57 +01:00
|
|
|
|
</template>
|
2025-11-03 08:42:24 +01:00
|
|
|
|
|
2025-11-14 17:45:57 +01:00
|
|
|
|
</AppHeader>
|
2025-11-11 11:49:38 +01:00
|
|
|
|
|
2025-11-14 17:45:57 +01:00
|
|
|
|
<!-- Invoice Table -->
|
2025-11-18 10:27:49 +01:00
|
|
|
|
<DocumentTable :invoices="filteredInvoices" :onItemClicked="editInvoice" />
|
2025-10-22 11:58:58 +02:00
|
|
|
|
|
2025-11-14 17:45:57 +01:00
|
|
|
|
<!-- Invoice detail dialog -->
|
2025-11-18 20:46:40 +01:00
|
|
|
|
<InvoiceDialog :invoiceData="activeInvoice" v-model="detailDialogOpen" @save="onSaveInvoice"
|
|
|
|
|
|
@delete="onDeleteInvoice" />
|
2025-10-20 08:57:51 +02:00
|
|
|
|
|
|
|
|
|
|
</AppLayout>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
@media print {
|
|
|
|
|
|
#function-header {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|