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

271 lines
10 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 { computed, ref, onMounted } from 'vue'
import { type Invoice } from '@/types'
import axios from 'axios'
import AppLayout from '@/layouts/AppLayout.vue'
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 Fuse from 'fuse.js';
import { Input } from '@/components/ui/input'
import { toast } from 'vue-sonner'
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'
import AppHeader from '@/components/AppHeader.vue'
import InvoiceDialog from '@/components/documents/InvoiceDialog.vue'
// initial invoice data from inertia
interface Props {
invoicesData: Invoice[];
}
const props = defineProps<Props>();
const invoicesData = ref(props.invoicesData || [])
const activeInvoice = ref<Invoice | undefined>(undefined)
const selectedYearIndex = ref(0)
const detailDialogOpen = ref(false)
const searchQuery = ref('')
const searchField = ref()
onMounted(async () => {
// 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)
}
let queryString = window.location.search
let params = new URLSearchParams(queryString)
if (params.get('action') == 'new') editInvoice()
searchField.value = document.getElementById('search')
})
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
})
// Filter / search
const fuse = computed(() => {
const options = {
keys: [
'billingData.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(() => {
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 => {
const invoiceDate = new Date(invoice.invoiceDate);
return !years.value[selectedYearIndex.value] || invoiceDate.getFullYear() === years.value[selectedYearIndex.value];
});
});
const editInvoice = (invoice?: Invoice) => {
// make a deep copy, so the changes in the dialog wont affect the data until saved
activeInvoice.value = invoice ? JSON.parse(JSON.stringify(invoice)) : null
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 {
await 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>
<!-- Function Header -->
<AppLayout title="Rechnungen">
<AppHeader>
<!-- Year select -->
<template #left>
<Button variant="ghost" :disabled="selectedYearIndex >= (years.length - 1)"
@click="selectedYearIndex++">
<ChevronLeft />
</Button>
<Select size="sm" v-model="selectedYearIndex">
<SelectTrigger class="hover:bg-accent">
<SelectValue :placeholder="(new Date()).getFullYear().toString()"
:disabled="years.length < 1" />
</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" />
</Button>
</span>
</template>
<!-- New button -->
<template #right>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button size="sm" variant="action" @click="editInvoice()">
<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>
</template>
</AppHeader>
<!-- Invoice Table -->
<DocumentTable :invoices="filteredInvoices" :onItemClicked="editInvoice" />
<!-- Invoice detail dialog -->
<InvoiceDialog :invoiceData="activeInvoice" v-model="detailDialogOpen" @save="" @delete="" />
</AppLayout>
</template>
<style>
@media print {
#function-header {
display: none;
}
}
</style>