- 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.
206 lines
6.7 KiB
Vue
206 lines
6.7 KiB
Vue
<template>
|
|
<div class="invoice-form">
|
|
<Message severity="info" v-if="!invoice">
|
|
Rechnungsformular (Phase 1 MVP - wird erweitert)
|
|
</Message>
|
|
|
|
<div class="grid p-fluid">
|
|
<!-- Rechnungsnummer -->
|
|
<div class="col-12 md:col-6">
|
|
<label for="invoiceNumber">Rechnungsnummer *</label>
|
|
<InputText id="invoiceNumber" v-model="form.invoiceNumber" required />
|
|
</div>
|
|
|
|
<!-- Status -->
|
|
<div class="col-12 md:col-6">
|
|
<label for="status">Status</label>
|
|
<Dropdown
|
|
id="status"
|
|
v-model="form.status"
|
|
:options="statusOptions"
|
|
option-label="label"
|
|
option-value="value"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Kunde -->
|
|
<div class="col-12 md:col-6">
|
|
<label for="contact">Kunde *</label>
|
|
<Dropdown
|
|
id="contact"
|
|
v-model="form.contactId"
|
|
:options="contacts"
|
|
option-label="name"
|
|
option-value="id"
|
|
filter
|
|
placeholder="Kunde auswählen"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Rechnungsdatum -->
|
|
<div class="col-12 md:col-6">
|
|
<label for="invoiceDate">Rechnungsdatum *</label>
|
|
<Calendar id="invoiceDate" v-model="form.invoiceDate" date-format="dd.mm.yy" />
|
|
</div>
|
|
|
|
<!-- Fälligkeitsdatum -->
|
|
<div class="col-12 md:col-6">
|
|
<label for="dueDate">Fälligkeitsdatum *</label>
|
|
<Calendar id="dueDate" v-model="form.dueDate" date-format="dd.mm.yy" />
|
|
</div>
|
|
|
|
<!-- Notizen -->
|
|
<div class="col-12">
|
|
<label for="notes">Notizen</label>
|
|
<Textarea id="notes" v-model="form.notes" rows="3" />
|
|
</div>
|
|
|
|
<!-- Positionen -->
|
|
<div class="col-12">
|
|
<h3>Rechnungspositionen</h3>
|
|
<DataTable :value="form.items" responsiveLayout="scroll">
|
|
<Column field="description" header="Beschreibung">
|
|
<template #body="slotProps">
|
|
<Textarea v-model="slotProps.data.description" rows="2" class="w-full" />
|
|
</template>
|
|
</Column>
|
|
<Column field="quantity" header="Menge">
|
|
<template #body="slotProps">
|
|
<InputNumber v-model="slotProps.data.quantity" :min-fraction-digits="2" class="w-full" />
|
|
</template>
|
|
</Column>
|
|
<Column field="unitPrice" header="Einzelpreis">
|
|
<template #body="slotProps">
|
|
<InputNumber v-model="slotProps.data.unitPrice" mode="currency" currency="EUR" locale="de-DE" class="w-full" />
|
|
</template>
|
|
</Column>
|
|
<Column field="taxRate" header="MwSt %">
|
|
<template #body="slotProps">
|
|
<Dropdown v-model="slotProps.data.taxRate" :options="taxRates" class="w-full" />
|
|
</template>
|
|
</Column>
|
|
<Column header="Aktionen">
|
|
<template #body="slotProps">
|
|
<Button icon="pi pi-trash" text severity="danger" @click="removeItem(slotProps.index)" />
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
|
|
<Button label="Position hinzufügen" icon="pi pi-plus" text @click="addItem" class="mt-2" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex gap-2 mt-4">
|
|
<Button label="Speichern" icon="pi pi-check" @click="save" />
|
|
<Button label="Abbrechen" icon="pi pi-times" severity="secondary" text @click="cancel" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import InputText from 'primevue/inputtext'
|
|
import Dropdown from 'primevue/dropdown'
|
|
import Calendar from 'primevue/calendar'
|
|
import Textarea from 'primevue/textarea'
|
|
import Button from 'primevue/button'
|
|
import DataTable from 'primevue/datatable'
|
|
import Column from 'primevue/column'
|
|
import InputNumber from 'primevue/inputnumber'
|
|
import Message from 'primevue/message'
|
|
|
|
const props = defineProps({
|
|
invoice: Object
|
|
})
|
|
|
|
const emit = defineEmits(['save', 'cancel'])
|
|
|
|
const form = ref({
|
|
invoiceNumber: '',
|
|
status: 'draft',
|
|
contactId: null,
|
|
invoiceDate: new Date(),
|
|
dueDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // +14 Tage
|
|
notes: '',
|
|
items: []
|
|
})
|
|
|
|
const contacts = ref([])
|
|
|
|
const statusOptions = [
|
|
{ label: 'Entwurf', value: 'draft' },
|
|
{ label: 'Offen', value: 'open' },
|
|
{ label: 'Bezahlt', value: 'paid' },
|
|
{ label: 'Teilweise bezahlt', value: 'partial' },
|
|
{ label: 'Überfällig', value: 'overdue' },
|
|
{ label: 'Storniert', value: 'cancelled' }
|
|
]
|
|
|
|
const taxRates = ['0.00', '7.00', '19.00']
|
|
|
|
onMounted(async () => {
|
|
// Load contacts
|
|
const response = await fetch('/api/contacts')
|
|
const data = await response.json()
|
|
contacts.value = data['hydra:member'] || []
|
|
|
|
// Load existing invoice data
|
|
if (props.invoice) {
|
|
form.value = { ...props.invoice }
|
|
if (props.invoice.invoiceDate) {
|
|
form.value.invoiceDate = new Date(props.invoice.invoiceDate)
|
|
}
|
|
if (props.invoice.dueDate) {
|
|
form.value.dueDate = new Date(props.invoice.dueDate)
|
|
}
|
|
}
|
|
})
|
|
|
|
const addItem = () => {
|
|
form.value.items.push({
|
|
description: '',
|
|
quantity: 1.00,
|
|
unitPrice: 0.00,
|
|
taxRate: '19.00'
|
|
})
|
|
}
|
|
|
|
const removeItem = (index) => {
|
|
form.value.items.splice(index, 1)
|
|
}
|
|
|
|
const save = async () => {
|
|
const method = props.invoice ? 'PUT' : 'POST'
|
|
const url = props.invoice ? `/api/invoices/${props.invoice.id}` : '/api/invoices'
|
|
|
|
const payload = {
|
|
invoiceNumber: form.value.invoiceNumber,
|
|
contact: `/api/contacts/${form.value.contactId}`,
|
|
status: form.value.status,
|
|
invoiceDate: form.value.invoiceDate.toISOString().split('T')[0],
|
|
dueDate: form.value.dueDate.toISOString().split('T')[0],
|
|
notes: form.value.notes,
|
|
items: form.value.items
|
|
}
|
|
|
|
await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
})
|
|
|
|
emit('save', form.value)
|
|
}
|
|
|
|
const cancel = () => {
|
|
emit('cancel')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.invoice-form {
|
|
padding: 1rem;
|
|
}
|
|
</style>
|