myCRM/assets/js/views/InvoiceManagement.vue
olli a787019a3b feat: Implement PDF upload functionality in PDFUploadForm.vue
- Added a file input for PDF uploads with validation for file type and size.
- Implemented file size formatting and user feedback for selected files.
- Created upload and cancel methods with placeholder for future API integration.

feat: Create PaymentForm.vue for handling payments

- Developed a form for entering payment details including date, amount, method, and notes.
- Integrated currency formatting and dropdown for payment methods.
- Implemented save and cancel actions with API call for saving payment data.

docs: Add documentation for dynamic plugin menus and permissions

- Provided guidelines for defining menu items and permissions in plugins.
- Explained the process for synchronizing permissions and integrating menus in the frontend.
- Included examples and best practices for plugin development.

feat: Add database migrations for invoices, invoice items, and payments

- Created migration scripts to define the database schema for invoices, invoice items, and payments.
- Established foreign key relationships between invoices and related entities.

feat: Implement command for synchronizing plugin permissions

- Developed a console command to synchronize plugin permissions with the database.
- Added options for dry-run and force synchronization for unlicensed modules.

feat: Create API controller for plugin menu items

- Implemented API endpoints to retrieve plugin menu items in both flat and grouped formats.
- Ensured access control with role-based permissions for API access.

feat: Develop service for managing plugin menu items

- Created a service to collect and manage menu items from installed plugins.
- Implemented methods for retrieving flat and grouped menu items for frontend use.

feat: Add service for synchronizing plugin permissions

- Developed a service to handle the synchronization of plugin permissions with the database.
- Included logic for creating and updating permission modules based on plugin definitions.
2025-12-05 11:13:41 +01:00

282 lines
9.2 KiB
Vue

<template>
<div class="card">
<h2>Rechnungsverwaltung</h2>
<CrudDataTable
title="Rechnungen"
:api-endpoint="`/api/invoices`"
:columns="columns"
:show-create-button="canCreate"
create-button-label="Neue Rechnung"
@create="openCreateDialog"
@edit="openEditDialog"
@delete="deleteInvoice"
>
<template #body="{ item }">
<Column field="invoiceNumber" header="Rechnungsnummer" sortable>
<template #body="slotProps">
<router-link :to="`/billing/invoices/${slotProps.data.id}`" class="text-primary">
{{ slotProps.data.invoiceNumber }}
</router-link>
</template>
</Column>
<Column field="contact.name" header="Kunde" sortable>
<template #body="slotProps">
{{ slotProps.data.contact?.name || '-' }}
</template>
</Column>
<Column field="invoiceDate" header="Rechnungsdatum" sortable>
<template #body="slotProps">
{{ formatDate(slotProps.data.invoiceDate) }}
</template>
</Column>
<Column field="dueDate" header="Fälligkeitsdatum" sortable>
<template #body="slotProps">
{{ formatDate(slotProps.data.dueDate) }}
</template>
</Column>
<Column field="total" header="Gesamtbetrag" sortable>
<template #body="slotProps">
{{ formatCurrency(slotProps.data.total) }}
</template>
</Column>
<Column field="openAmount" header="Offen" sortable>
<template #body="slotProps">
{{ formatCurrency(slotProps.data.openAmount) }}
</template>
</Column>
<Column field="status" header="Status" sortable>
<template #body="slotProps">
<Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
</template>
</Column>
<Column :exportable="false" style="min-width:12rem">
<template #body="slotProps">
<div class="flex gap-2">
<Button
v-if="slotProps.data.pdfPath"
icon="pi pi-file-pdf"
outlined
severity="secondary"
@click="viewPDF(slotProps.data)"
v-tooltip.top="'PDF anzeigen'"
/>
<Button
v-else
icon="pi pi-upload"
outlined
severity="info"
@click="uploadPDF(slotProps.data)"
v-tooltip.top="'PDF hochladen'"
/>
<Button
icon="pi pi-money-bill"
outlined
severity="success"
@click="addPayment(slotProps.data)"
v-tooltip.top="'Zahlung hinzufügen'"
/>
<Button
icon="pi pi-pencil"
outlined
severity="info"
@click="openEditDialog(slotProps.data)"
v-tooltip.top="'Bearbeiten'"
/>
<Button
icon="pi pi-trash"
outlined
severity="danger"
@click="deleteInvoice(slotProps.data)"
v-tooltip.top="'Löschen'"
/>
</div>
</template>
</Column>
</template>
</CrudDataTable>
<!-- Create/Edit Dialog -->
<Dialog v-model:visible="dialogVisible" :header="dialogTitle" :modal="true" :style="{width: '70vw'}" :maximizable="true">
<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 = [
{ field: 'invoiceNumber', header: 'Rechnungsnummer' },
{ field: 'contact.name', header: 'Kunde' },
{ field: 'invoiceDate', header: 'Rechnungsdatum' },
{ field: 'dueDate', header: 'Fälligkeitsdatum' },
{ field: 'total', header: 'Gesamtbetrag' },
{ field: 'openAmount', header: 'Offen' },
{ field: 'status', header: 'Status' }
]
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;
}
</style>