Files
Caramel-CRM/resources/js/pages/Invoices.vue
T

223 lines
8.2 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 { 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 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[])
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')
})
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 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>
<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="createInvoice">
<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="onSaveInvoice"
@delete="onDeleteInvoice" />
</AppLayout>
</template>
<style>
@media print {
#function-header {
display: none;
}
}
</style>