- 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.
282 lines
9.2 KiB
Vue
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>
|