myCRM/assets/js/views/InvoiceManagement.vue
olli 82b022ba3b Add comprehensive styles for invoice form components and utilities
- 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.
2025-12-13 10:02:30 +01:00

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>