This repository has been archived on 2025-12-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Caramel-CRM-Backup/resources/js/pages/Invoices.vue
T

293 lines
12 KiB
Vue
Raw Normal View History

2025-10-20 08:57:51 +02:00
<script setup lang="ts">
import AppLayout from '@/layouts/AppLayout.vue'
import { invoices } from '@/routes'
import { type Invoice, type BreadcrumbItem, type Customer, type Address } from '@/types'
import { newInvoice } from '@/types/index.d'
import { Head } from '@inertiajs/vue3'
import { computed, ref, onMounted, watch } from 'vue'
import axios, { Axios, AxiosError, AxiosResponse } from 'axios'
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 InvoiceDialog from '@/components/documents/InvoiceDialog.vue'
import Fuse from 'fuse.js';
import { Input } from '@/components/ui/input'
2025-10-29 15:42:43 +01:00
import { toast } from 'vue-sonner'
import { testToast } from '@/lib/utils'
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'
import { statusBadgeLabels } from '@/components/ui/status-badge'
2025-10-20 08:57:51 +02:00
const breadcrumbs: BreadcrumbItem[] = [{
title: 'Rechnungsstellung',
href: invoices().url,
}]
const invoicesData = ref([] as Invoice[])
const activeInvoice = ref<Invoice | null>(null)
const customersData = ref([] as Customer[])
const selectedYearIndex = ref(0)
const detailDialogOpen = ref(false)
const searchQuery = ref('')
const searchField = ref()
onMounted(async () => {
const invoiceResponse = await axios.get('/api/invoices')
invoicesData.value = invoiceResponse.data as Invoice[]
2025-10-20 08:57:51 +02:00
const customerResponse = await axios.get('/api/customers')
customersData.value = customerResponse.data as Customer[]
searchField.value = document.getElementById('search')
let queryString = window.location.search
let params = new URLSearchParams(queryString)
if (params.get('action') == 'new') showDetail(newInvoice())
2025-10-20 08:57:51 +02:00
})
watch(invoicesData, () => {
// Die watch-Funktion wird automatisch ausgelöst, wenn sich invoicesData ändert
// Das filteredInvoices-Array wird automatisch aktualisiert
// Fügen Sie eine kleine Verzögerung hinzu, um sicherzustellen, dass die Daten aktualisiert werden
setTimeout(() => {
// Die watch-Funktion wird automatisch ausgelöst, wenn sich invoicesData ändert
// Das filteredInvoices-Array wird automatisch aktualisiert
}, 0);
}, { deep: true });
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
})
const fuse = computed(() => {
const options = {
keys: [
'customer.companyName',
{
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(() => {
// Filter by query
if (!searchQuery.value) {
return years.value[selectedYearIndex.value] ?
invoicesData.value.filter(invoice => {
const invoiceDate = new Date(invoice.invoiceDate);
return invoiceDate.getFullYear() === years.value[selectedYearIndex.value];
}) :
invoicesData.value;
}
return fuse.value.search(searchQuery.value)
.map(result => result.item)
.filter(invoice => {
// Stellen Sie sicher, dass die gefilterten Rechnungen auch zum ausgewählten Jahr gehören
const invoiceDate = new Date(invoice.invoiceDate);
return !years.value[selectedYearIndex.value] || invoiceDate.getFullYear() === years.value[selectedYearIndex.value];
});
});
const showDetail = (invoice: Invoice) => {
// make a deep copy, so the changes in the dialog wont affect the data until saved
activeInvoice.value = JSON.parse(JSON.stringify(invoice))
detailDialogOpen.value = true
}
const saveInvoice = async (updatedInvoice: Invoice) => {
try {
// Prepare the invoice data for API request
const invoiceToSave = {
nr: updatedInvoice.nr,
invoiceDate: updatedInvoice.invoiceDate,
dueDate: updatedInvoice.dueDate,
serviceStartDate: updatedInvoice.serviceStartDate,
serviceEndDate: updatedInvoice.serviceEndDate,
isRecurring: updatedInvoice.isRecurring,
isPartialService: updatedInvoice.isPartialService,
paymentStatus: updatedInvoice.paymentStatus,
totalAmount: updatedInvoice.totalAmount,
title: updatedInvoice.title,
text: updatedInvoice.text,
customerId: updatedInvoice.customer ? updatedInvoice.customer.id : null,
2025-10-20 08:57:51 +02:00
billingData: {
companyName: updatedInvoice.billingData?.companyName,
vatId: updatedInvoice.billingData?.vatId,
billingAddress: updatedInvoice.billingData?.billingAddress,
2025-10-29 13:53:08 +01:00
contactSalutation: updatedInvoice.billingData?.contactSalutation,
2025-10-20 08:57:51 +02:00
contactFirstName: updatedInvoice.billingData?.contactFirstName,
contactLastName: updatedInvoice.billingData?.contactLastName,
paymentTerms: updatedInvoice.billingData?.paymentTerms
},
// Items will be handled separately in the controller
items: updatedInvoice.items.map(item => ({
id: item.id, // Include ID for existing items
position: item.position,
type: item.type,
title: item.title,
description: item.description,
quantity: item.quantity,
unit: item.unit,
price: item.price
}))
};
// console.log('Saving invoice:', invoiceToSave);
if (updatedInvoice.id === 0) {
// Create new invoice
const response = await axios.post('/api/invoices', invoiceToSave);
invoicesData.value.push(response.data);
// Update selectedYearIndex to ensure the new invoice is displayed
const newInvoiceYear = new Date(response.data.invoiceDate).getFullYear();
const currentYearIndex = years.value.findIndex(year => year === newInvoiceYear);
if (currentYearIndex !== -1) {
selectedYearIndex.value = currentYearIndex;
}
} else {
// Update existing invoice
const response = await axios.put(`/api/invoices/${updatedInvoice.id}`, invoiceToSave);
const index = invoicesData.value.findIndex(inv => inv.id === updatedInvoice.id);
if (index !== -1) {
invoicesData.value[index] = response.data;
}
}
} catch (error) {
toast.error("Rechnung konnte nicht gespeichert werden", {
description: (error as Error).message,
});
}
}
const deleteInvoice = async (id: number) => {
try {
const response = axios.delete('/api/invoices/' + id)
const index = invoicesData.value.findIndex(invoice => invoice.id === id);
if (index !== -1) {
invoicesData.value.splice(index, 1)
}
} catch (error) {
toast.error("Rechnung konnte nicht gelöscht werden", {
description: (error as Error).message,
});
}
}
</script>
<template>
<Head title="Rechnungen" />
2025-11-11 11:49:38 +01:00
<!-- Function Header -->
2025-10-20 08:57:51 +02:00
<AppLayout :breadcrumbs="breadcrumbs">
2025-10-29 15:42:43 +01:00
2025-10-20 08:57:51 +02:00
<div
2025-11-11 11:49:38 +01:00
class="flex h-full flex-1 flex-col gap-4 overflow-x-auto p-4 lg:p-8 lg:pl-4 print:bg-transparent print:p-0 print:m-0">
2025-10-20 08:57:51 +02:00
2025-11-03 08:42:24 +01:00
<div id="function-header" class="flex row justify-between items-center mb-4 gap-4">
2025-10-20 08:57:51 +02:00
<!-- Year select -->
<div class="flex row items-center">
<Button :variant="'ghost'" :disabled="selectedYearIndex >= (years.length - 1)"
@click="selectedYearIndex++">
<ChevronLeft />
</Button>
<Select :size="'sm'" v-model="selectedYearIndex" v-if="years.length > 1">
2025-11-11 11:49:38 +01:00
<SelectTrigger class=" hover:bg-accent">
2025-10-20 08:57:51 +02:00
<SelectValue placeholder="Jahr" />
</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>
</div>
<!-- Search field -->
2025-10-20 08:57:51 +02:00
<div class="relative w-full max-w-sm items-center">
<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" />
</Button>
</span>
</div>
<!-- <Button size="sm" @click="testToast" class="mr-2">
2025-11-11 11:49:38 +01:00
Toast
</Button> -->
2025-10-20 08:57:51 +02:00
2025-11-03 08:42:24 +01:00
<!-- New button -->
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button size="sm" variant="action" @click="showDetail(newInvoice())">
<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-10-20 08:57:51 +02:00
</div>
2025-11-11 11:49:38 +01:00
2025-10-20 08:57:51 +02:00
<!-- Invoice Table -->
<DocumentTable :invoices="filteredInvoices" :onItemClicked="showDetail" />
2025-10-20 08:57:51 +02:00
<!-- Invoice detail dialog -->
<InvoiceDialog :invoiceData="activeInvoice" :customers="customersData" v-model="detailDialogOpen"
@save="saveInvoice" @delete="deleteInvoice" />
</div>
2025-10-20 08:57:51 +02:00
</AppLayout>
</template>
<style>
@media print {
#function-header {
display: none;
}
}
</style>