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

289 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import AppLayout from '@/layouts/AppLayout.vue'
import { invoices } from '@/routes'
import { type Invoice, 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'
import { ChevronLeft, ChevronRight, Plus, Search, Delete } from "lucide-vue-next"
import InvoiceDialog from '@/components/documents/InvoiceDialog.vue'
import Fuse from 'fuse.js';
import { Input } from '@/components/ui/input'
import { toast } from 'vue-sonner'
import { testToast } from '@/lib/utils'
import SelectSeparator from '@/components/ui/select/SelectSeparator.vue'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { statusBadgeLabels } from '@/components/ui/status-badge'
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[]
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())
})
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,
}
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,
billingData: {
companyName: updatedInvoice.billingData?.companyName,
vatId: updatedInvoice.billingData?.vatId,
billingAddress: updatedInvoice.billingData?.billingAddress,
contactSalutation: updatedInvoice.billingData?.contactSalutation,
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" />
<!-- Function Header -->
<AppLayout>
<div
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">
<div id="function-header" class="flex row justify-between items-center mb-4 gap-4">
<!-- 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">
<SelectTrigger class=" hover:bg-accent">
<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 -->
<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">
Toast
</Button> -->
<!-- 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>
</div>
<!-- Invoice Table -->
<DocumentTable :invoices="filteredInvoices" :onItemClicked="showDetail" />
<!-- Invoice detail dialog -->
<InvoiceDialog :invoiceData="activeInvoice" :customers="customersData" v-model="detailDialogOpen"
@save="saveInvoice" @delete="deleteInvoice" />
</div>
</AppLayout>
</template>
<style>
@media print {
#function-header {
display: none;
}
}
</style>