- Introduced CSS custom properties for spacing, typography, colors, and shadows. - Developed styles for form sections, grids, invoice items, and summary components. - Implemented responsive design adjustments for various screen sizes. - Added utility classes for text, spacing, and flex layouts. - Included dark mode and high contrast mode support. - Established loading states and validation/error styles. - Enhanced accessibility features with focus styles and screen reader utilities.
299 lines
8.9 KiB
Vue
299 lines
8.9 KiB
Vue
<template>
|
|
<div class="card">
|
|
<h2>Rechnungsverwaltung</h2>
|
|
|
|
<CrudDataTable
|
|
title="Rechnungen"
|
|
entity-name="Rechnung"
|
|
entity-name-article="eine"
|
|
data-source="/api/invoices"
|
|
storage-key="invoiceTableColumns"
|
|
:columns="columns"
|
|
:show-create-button="canCreate"
|
|
@create="openCreateDialog"
|
|
@edit="openEditDialog"
|
|
@delete="deleteInvoice"
|
|
>
|
|
<template #body-invoiceNumber="{ data }">
|
|
<router-link :to="`/billing/invoices/${data.id}`" class="text-primary">
|
|
{{ data.invoiceNumber }}
|
|
</router-link>
|
|
</template>
|
|
<template #body-contact.companyName="{ data }">
|
|
{{ data.contact?.companyName || '-' }}
|
|
</template>
|
|
<template #body-invoiceDate="{ data }">
|
|
{{ formatDate(data.invoiceDate) }}
|
|
</template>
|
|
<template #body-dueDate="{ data }">
|
|
{{ formatDate(data.dueDate) }}
|
|
</template>
|
|
<template #body-total="{ data }">
|
|
{{ formatCurrency(data.total) }}
|
|
</template>
|
|
<template #body-openAmount="{ data }">
|
|
{{ formatCurrency(data.openAmount) }}
|
|
</template>
|
|
<template #body-status="{ data }">
|
|
<Tag :value="getStatusLabel(data.status)" :severity="getStatusSeverity(data.status)" />
|
|
</template>
|
|
<template #body-actions="{ data }">
|
|
<div class="flex gap-2">
|
|
<Button
|
|
v-if="data.pdfPath"
|
|
icon="pi pi-file-pdf"
|
|
outlined
|
|
severity="secondary"
|
|
@click="viewPDF(data)"
|
|
v-tooltip.top="'PDF anzeigen'"
|
|
/>
|
|
<Button
|
|
v-else
|
|
icon="pi pi-upload"
|
|
outlined
|
|
severity="info"
|
|
@click="uploadPDF(data)"
|
|
v-tooltip.top="'PDF hochladen'"
|
|
/>
|
|
<Button
|
|
icon="pi pi-money-bill"
|
|
outlined
|
|
severity="success"
|
|
@click="addPayment(data)"
|
|
v-tooltip.top="'Zahlung hinzufügen'"
|
|
/>
|
|
<Button
|
|
icon="pi pi-pencil"
|
|
outlined
|
|
severity="info"
|
|
@click="openEditDialog(data)"
|
|
v-tooltip.top="'Bearbeiten'"
|
|
/>
|
|
<Button
|
|
icon="pi pi-trash"
|
|
outlined
|
|
severity="danger"
|
|
@click="deleteInvoice(data)"
|
|
v-tooltip.top="'Löschen'"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</CrudDataTable>
|
|
|
|
<!-- Create/Edit Dialog -->
|
|
<Dialog
|
|
v-model:visible="dialogVisible"
|
|
:header="dialogTitle"
|
|
:modal="true"
|
|
:style="{width: '90vw', maxWidth: '1200px'}"
|
|
:maximizable="true"
|
|
:dismissableMask="true"
|
|
class="invoice-dialog"
|
|
>
|
|
<InvoiceForm
|
|
v-if="dialogVisible"
|
|
:invoice="currentInvoice"
|
|
@save="saveInvoice"
|
|
@cancel="closeDialog"
|
|
/>
|
|
</Dialog>
|
|
|
|
<!-- Payment Dialog -->
|
|
<Dialog v-model:visible="paymentDialogVisible" header="Zahlung hinzufügen" :modal="true" :style="{width: '40vw'}">
|
|
<PaymentForm
|
|
v-if="paymentDialogVisible"
|
|
:invoice="currentInvoice"
|
|
@save="savePayment"
|
|
@cancel="paymentDialogVisible = false"
|
|
/>
|
|
</Dialog>
|
|
|
|
<!-- PDF Upload Dialog -->
|
|
<Dialog v-model:visible="pdfUploadDialogVisible" header="PDF hochladen" :modal="true" :style="{width: '40vw'}">
|
|
<PDFUploadForm
|
|
v-if="pdfUploadDialogVisible"
|
|
:invoice="currentInvoice"
|
|
@save="savePDFUpload"
|
|
@cancel="pdfUploadDialogVisible = false"
|
|
/>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import CrudDataTable from '@/components/CrudDataTable.vue'
|
|
import Column from 'primevue/column'
|
|
import Button from 'primevue/button'
|
|
import Dialog from 'primevue/dialog'
|
|
import Tag from 'primevue/tag'
|
|
import InvoiceForm from './InvoiceForm.vue'
|
|
import PaymentForm from './PaymentForm.vue'
|
|
import PDFUploadForm from './PDFUploadForm.vue'
|
|
|
|
const router = useRouter()
|
|
const authStore = useAuthStore()
|
|
|
|
const dialogVisible = ref(false)
|
|
const dialogTitle = ref('')
|
|
const currentInvoice = ref(null)
|
|
const paymentDialogVisible = ref(false)
|
|
const pdfUploadDialogVisible = ref(false)
|
|
|
|
const canCreate = computed(() => authStore.hasPermission('billing', 'create'))
|
|
|
|
const columns = [
|
|
{ key: 'invoiceNumber', field: 'invoiceNumber', label: 'Rechnungsnummer', default: true },
|
|
{ key: 'contact.companyName', field: 'contact.companyName', label: 'Kunde', default: true },
|
|
{ key: 'invoiceDate', field: 'invoiceDate', label: 'Rechnungsdatum', default: true },
|
|
{ key: 'dueDate', field: 'dueDate', label: 'Fälligkeitsdatum', default: true },
|
|
{ key: 'total', field: 'total', label: 'Gesamtbetrag', default: true },
|
|
{ key: 'openAmount', field: 'openAmount', label: 'Offen', default: true },
|
|
{ key: 'status', field: 'status', label: 'Status', default: true }
|
|
]
|
|
|
|
const openCreateDialog = () => {
|
|
currentInvoice.value = null
|
|
dialogTitle.value = 'Neue Rechnung'
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
const openEditDialog = (invoice) => {
|
|
currentInvoice.value = invoice
|
|
dialogTitle.value = 'Rechnung bearbeiten'
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
const closeDialog = () => {
|
|
dialogVisible.value = false
|
|
currentInvoice.value = null
|
|
}
|
|
|
|
const saveInvoice = async (invoice) => {
|
|
// Save logic handled by InvoiceForm
|
|
closeDialog()
|
|
// Refresh table
|
|
window.location.reload()
|
|
}
|
|
|
|
const deleteInvoice = async (invoice) => {
|
|
if (confirm(`Rechnung ${invoice.invoiceNumber} wirklich löschen?`)) {
|
|
await fetch(`/api/invoices/${invoice.id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
window.location.reload()
|
|
}
|
|
}
|
|
|
|
const addPayment = (invoice) => {
|
|
currentInvoice.value = invoice
|
|
paymentDialogVisible.value = true
|
|
}
|
|
|
|
const savePayment = async () => {
|
|
paymentDialogVisible.value = false
|
|
window.location.reload()
|
|
}
|
|
|
|
const uploadPDF = (invoice) => {
|
|
currentInvoice.value = invoice
|
|
pdfUploadDialogVisible.value = true
|
|
}
|
|
|
|
const savePDFUpload = async () => {
|
|
pdfUploadDialogVisible.value = false
|
|
window.location.reload()
|
|
}
|
|
|
|
const viewPDF = (invoice) => {
|
|
window.open(invoice.pdfPath, '_blank')
|
|
}
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return '-'
|
|
const date = new Date(dateString)
|
|
return date.toLocaleDateString('de-DE')
|
|
}
|
|
|
|
const formatCurrency = (amount) => {
|
|
if (!amount) return '0,00 €'
|
|
return new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(parseFloat(amount))
|
|
}
|
|
|
|
const getStatusLabel = (status) => {
|
|
const labels = {
|
|
draft: 'Entwurf',
|
|
open: 'Offen',
|
|
paid: 'Bezahlt',
|
|
partial: 'Teilweise bezahlt',
|
|
overdue: 'Überfällig',
|
|
cancelled: 'Storniert'
|
|
}
|
|
return labels[status] || status
|
|
}
|
|
|
|
const getStatusSeverity = (status) => {
|
|
const severities = {
|
|
draft: 'secondary',
|
|
open: 'info',
|
|
paid: 'success',
|
|
partial: 'warning',
|
|
overdue: 'danger',
|
|
cancelled: 'secondary'
|
|
}
|
|
return severities[status] || 'info'
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.text-primary {
|
|
color: var(--primary-color);
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.text-primary:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Dialog adjustments */
|
|
.invoice-dialog :deep(.p-dialog-content) {
|
|
padding: 0;
|
|
background: var(--surface-ground);
|
|
}
|
|
|
|
.invoice-dialog :deep(.p-dialog-header) {
|
|
background: var(--primary-color);
|
|
color: white;
|
|
border-radius: 0;
|
|
}
|
|
|
|
.invoice-dialog :deep(.p-dialog-header .p-dialog-title) {
|
|
font-weight: 600;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.invoice-dialog :deep(.p-dialog-header-icons button) {
|
|
color: white !important;
|
|
}
|
|
|
|
.invoice-dialog :deep(.p-dialog-header-icons button:hover) {
|
|
background: rgba(255, 255, 255, 0.1) !important;
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.invoice-dialog :deep(.p-dialog) {
|
|
width: 95vw !important;
|
|
max-width: none !important;
|
|
}
|
|
}
|
|
</style>
|