Files
Caramel-CRM/resources/js/pages/Invoices.vue
T
2026-02-17 10:35:03 +01:00

262 lines
9.8 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 { newInvoice } from '@/types/index.d'
import axios from 'axios'
import AppLayout from '@/layouts/AppLayout.vue'
import AppHeader from '@/components/AppHeader.vue'
import { Button } from '@/components/ui/crm-button'
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'
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/crm-input'
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 InvoiceDialog from '@/components/documents/InvoiceDialog.vue'
import { hotkey, getPlatformModifierSymbol } from '@/lib/utils'
// Initial invoice data from inertia (see InvoiceController::show)
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[])
invoicesData.value = invoicesData.value.sort(
(a, b) => new Date(a.invoiceDate).getTime() - new Date(b.invoiceDate).getTime()
)
} 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') createInvoice()
searchField.value = document.getElementById('search')
// Register hotkeys
hotkey('n', createInvoice)
hotkey('mod+leftarrow', () => {
if (selectedYearIndex.value < (years.value.length - 1)) {
selectedYearIndex.value++
}
})
hotkey('mod+rightarrow', () => {
if (selectedYearIndex.value > 0) {
selectedYearIndex.value--
}
})
hotkey('a', () => { selectedYearIndex.value = -1 })
hotkey('mod+f', () => { searchField.value.focus() })
})
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'
],
useExtendedSearch: true,
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 createInvoice = () => {
editInvoice(newInvoice())
}
const editInvoice = (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 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;
}
// Update table
const index = invoicesData.value.findIndex(inv => inv.id === updatedInvoice.id);
if (index !== -1) {
invoicesData.value[index] = updatedInvoice;
} else {
invoicesData.value.push(updatedInvoice);
}
}
const onDeleteInvoice = async (id: number) => {
const index = invoicesData.value.findIndex(invoice => invoice.id === id)
if (index !== -1) {
invoicesData.value.splice(index, 1)
}
}
</script>
<template>
<!-- Function Header -->
<AppLayout title="Rechnungen">
<AppHeader>
<!-- Year select -->
<template #left>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button variant="ghost" :disabled="selectedYearIndex >= (years.length - 1)"
@click="selectedYearIndex++">
<ChevronLeft />
</Button>
</TooltipTrigger>
<TooltipContent>
<span>Ein Jahr zurück</span>
<KbdGroup class="ml-2">
<Kbd>{{ getPlatformModifierSymbol() }}</Kbd>
<Kbd>&larr;</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
<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="-1">
<SelectLabel>Alle</SelectLabel>
</SelectItem>
<SelectSeparator />
<SelectItem v-for="(year, index) in years" :value="index">
<SelectLabel>{{ year }}</SelectLabel>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Tooltip>
<TooltipTrigger>
<Button variant="ghost" @click="selectedYearIndex--" :disabled="selectedYearIndex <= 0">
<ChevronRight />
</Button>
</TooltipTrigger>
<TooltipContent>
<span>Ein Jahr vorwärts</span>
<KbdGroup class="ml-2">
<Kbd>{{ getPlatformModifierSymbol() }}</Kbd>
<Kbd>&rarr;</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</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" />
</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" />
</Button>
</span>
</template>
<!-- New button -->
<template #right>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button @click="createInvoice">
<Plus />
Neu
</Button>
</TooltipTrigger>
<TooltipContent>
<span>Neue Rechnung anlegen</span>
<KbdGroup class="ml-2">
<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="onSaveInvoice"
@delete="onDeleteInvoice" />
</AppLayout>
</template>
<style>
@media print {
#function-header {
display: none;
}
}
</style>