Add initial version of Manual Invoice functionality with PDF generation and management
This commit is contained in:
@@ -28,10 +28,40 @@
|
||||
.invoice-preview-pane {
|
||||
flex: 1 1 auto;
|
||||
background-color: #525659;
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pdf-preview-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pdf-preview-container object {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pdf-loading {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.pdf-loading i {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pdf-loading p {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.info-bar {
|
||||
|
||||
@@ -3,18 +3,28 @@ Vue.component('manual-invoice', {
|
||||
<tt-card>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<tt-button text="Neue Rechnung" icon="fas fa-plus" @click="openModal()" additional-class="btn-primary"/>
|
||||
<tt-button text="Test Prefill & Reload" icon="fas fa-magic" @click="testPrefill" additional-class="btn-info"/>
|
||||
</div>
|
||||
|
||||
<tt-table-crud
|
||||
ref="table"
|
||||
emit-edit
|
||||
@edit="openModal($event)">
|
||||
<template v-slot:totalamount="{ row }">
|
||||
{{ formatPrice(row.totalAmount) }}
|
||||
<template v-slot:total="{ row }">
|
||||
{{ formatPrice(row.total) }}
|
||||
</template>
|
||||
<template v-slot:invoicedate="{ row }">
|
||||
{{ formatDate(row.invoiceDate) }}
|
||||
<template v-slot:total_gross="{ row }">
|
||||
{{ formatPrice(row.total_gross) }}
|
||||
</template>
|
||||
<template v-slot:invoice_date="{ row }">
|
||||
{{ formatDate(row.invoice_date) }}
|
||||
</template>
|
||||
<template v-slot:customerName="{ row }">
|
||||
{{ row.customerName }}
|
||||
</template>
|
||||
<template v-slot:actions="{ row }">
|
||||
<button class="btn btn-sm btn-primary" @click="downloadPdf(row.id)" title="PDF herunterladen">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
</button>
|
||||
</template>
|
||||
</tt-table-crud>
|
||||
|
||||
@@ -32,19 +42,6 @@ Vue.component('manual-invoice', {
|
||||
editingInvoiceData: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const prefillData = localStorage.getItem('ManualInvoice_create');
|
||||
if (prefillData) {
|
||||
try {
|
||||
this.editingInvoiceData = JSON.parse(prefillData);
|
||||
this.isModalOpen = true;
|
||||
} catch (e) {
|
||||
console.error("Failed to parse prefill data:", e);
|
||||
} finally {
|
||||
localStorage.removeItem('ManualInvoice_create');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openModal(invoice = null) {
|
||||
this.editingInvoiceData = invoice ? JSON.parse(JSON.stringify(invoice)) : null;
|
||||
@@ -55,30 +52,75 @@ Vue.component('manual-invoice', {
|
||||
this.editingInvoiceData = null;
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
},
|
||||
handleSave(invoiceData) {
|
||||
console.log("--- INVOICE SAVED (DEMO) ---");
|
||||
console.log(JSON.parse(JSON.stringify(invoiceData)));
|
||||
window.notify('success', 'Rechnung in der Konsole geloggt!');
|
||||
this.closeModal();
|
||||
async handleSave(invoiceData) {
|
||||
try {
|
||||
// Calculate totals for each position
|
||||
const positions = invoiceData.positions.map(p => {
|
||||
const amount = parseFloat(p.amount) || 0;
|
||||
const price = parseFloat(p.price) || 0;
|
||||
const vatrate = parseFloat(p.vatrate) || 0;
|
||||
const price_total = amount * price;
|
||||
const price_gross = price_total * (1 + vatrate / 100);
|
||||
|
||||
return {
|
||||
...p,
|
||||
amount,
|
||||
price,
|
||||
vatrate,
|
||||
price_total,
|
||||
price_gross,
|
||||
product_id: 0,
|
||||
contract_id: 0,
|
||||
billing_id: 0,
|
||||
billing_period: 0
|
||||
};
|
||||
});
|
||||
|
||||
// Prepare invoice data
|
||||
const payload = {
|
||||
id: invoiceData.id || null,
|
||||
invoice_number: invoiceData.invoice_number,
|
||||
invoice_date: invoiceData.invoice_date,
|
||||
owner_id: invoiceData.owner_id || 0,
|
||||
billingaddress_id: invoiceData.billingaddress_id || 0,
|
||||
customer_number: invoiceData.customer_number || 0,
|
||||
company: invoiceData.company || '',
|
||||
firstname: invoiceData.firstname || '',
|
||||
lastname: invoiceData.lastname || '',
|
||||
street: invoiceData.street || '',
|
||||
zip: invoiceData.zip || '',
|
||||
city: invoiceData.city || '',
|
||||
country: invoiceData.country || 'Österreich',
|
||||
email: invoiceData.email || '',
|
||||
uid: invoiceData.uid || '',
|
||||
billing_type: invoiceData.billing_type || 'invoice',
|
||||
billing_delivery: 'email',
|
||||
tax_text: invoiceData.tax_text || '',
|
||||
fibu_payment_due: 14,
|
||||
fibu_account_number: invoiceData.fibu_account_number || 0,
|
||||
vatgroup_id: 1,
|
||||
positions: positions
|
||||
};
|
||||
|
||||
const url = invoiceData.id
|
||||
? window.TT_CONFIG.UPDATE_URL
|
||||
: window.TT_CONFIG.CREATE_URL;
|
||||
|
||||
const response = await axios.post(url, payload);
|
||||
|
||||
if (response.data.success) {
|
||||
window.notify('success', response.data.message || 'Rechnung erfolgreich gespeichert!');
|
||||
this.closeModal();
|
||||
} else {
|
||||
window.notify('error', response.data.message || 'Fehler beim Speichern der Rechnung');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving invoice:', error);
|
||||
window.notify('error', 'Fehler beim Speichern der Rechnung: ' + (error.response?.data?.message || error.message));
|
||||
}
|
||||
},
|
||||
testPrefill() {
|
||||
const mockInvoice = {
|
||||
id: null,
|
||||
invoiceNumber: `RE-${new Date().getFullYear()}-XXXX`,
|
||||
invoiceDate: moment().unix(),
|
||||
dueDate: moment().add(14, 'days').unix(),
|
||||
status: 'draft',
|
||||
billingAddressId: 1, // Example ID for autocomplete to fetch
|
||||
customer: {}, // Will be populated by watcher
|
||||
positions: [
|
||||
{ product_name: 'Stunden Techniker', product_info: 'Arbeiten an Server-Infrastruktur', start_date: moment().format('YYYY-MM-DD'), end_date: moment().format('YYYY-MM-DD'), amount: 3.5, price: 95.00, vatrate: 20 },
|
||||
{ product_name: 'Anfahrtspauschale', product_info: '', start_date: moment().format('YYYY-MM-DD'), end_date: moment().format('YYYY-MM-DD'), amount: 1, price: 45.00, vatrate: 20 }
|
||||
],
|
||||
closingText: 'Wir bedanken uns für die gute Zusammenarbeit.',
|
||||
taxText: ''
|
||||
};
|
||||
localStorage.setItem('ManualInvoice_create', JSON.stringify(mockInvoice));
|
||||
window.location.reload();
|
||||
downloadPdf(invoiceId) {
|
||||
window.location.href = `${window.TT_CONFIG.BASE_PATH}/ManualInvoice/downloadInvoicePdf?id=${invoiceId}`;
|
||||
},
|
||||
formatPrice(value) {
|
||||
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value || 0);
|
||||
@@ -93,7 +135,7 @@ Vue.component('manual-invoice', {
|
||||
Vue.component('manual-invoice-modal', {
|
||||
props: ['initialData'],
|
||||
template: `
|
||||
<div class="manual-invoice-overlay" :class="overlayClasses" @keydown.ctrl.q.prevent="togglePreviewVisibility" tabindex="-1" ref="overlay">
|
||||
<div class="manual-invoice-overlay" :class="overlayClasses" tabindex="-1" ref="overlay">
|
||||
<div class="info-bar" v-if="!isLargeScreen">
|
||||
<i class="fas fa-info-circle mr-2"></i> Drücke <strong>STRG + Q</strong> um die Vorschau umzuschalten.
|
||||
</div>
|
||||
@@ -102,21 +144,21 @@ Vue.component('manual-invoice-modal', {
|
||||
<div class="editor-header">
|
||||
<h3>{{ isCreateMode ? 'Neue Rechnung' : 'Rechnung bearbeiten' }}</h3>
|
||||
<div class="editor-actions">
|
||||
<tt-button text="Speichern" icon="fas fa-save" @click="$emit('save', invoiceData)" additional-class="btn-success"/>
|
||||
<tt-button text="Speichern" icon="fas fa-save" @click="saveInvoice" additional-class="btn-success"/>
|
||||
<tt-button text="Schließen" icon="fas fa-times" @click="close" additional-class="btn-secondary"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-content">
|
||||
<tt-card>
|
||||
<template v-slot:header><h5><i class="fas fa-user-tie mr-2"></i>Kunde</h5></template>
|
||||
<tt-autocomplete label="Kunde suchen" :api-url="customerApiUrl" v-model="invoiceData.billingAddressId" sm row />
|
||||
<tt-autocomplete label="Kunde suchen" :api-url="customerApiUrl" v-model="invoiceData.billingaddress_id" sm row />
|
||||
</tt-card>
|
||||
<tt-card>
|
||||
<template v-slot:header><h5><i class="fas fa-file-invoice mr-2"></i>Rechnungsdetails</h5></template>
|
||||
<div class="form-grid">
|
||||
<tt-input label="Rechnungsnr." v-model="invoiceData.invoiceNumber" sm/>
|
||||
<tt-date-picker label="Rechnungsdatum" v-model="invoiceData.invoiceDate" :date-range="false" sm/>
|
||||
<tt-date-picker label="Fälligkeitsdatum" v-model="invoiceData.dueDate" :date-range="false" sm/>
|
||||
<tt-input label="Rechnungsnr." v-model="invoiceData.invoice_number" sm/>
|
||||
<tt-date-picker label="Rechnungsdatum" v-model="invoiceData.invoice_date" :date-range="false" sm/>
|
||||
<tt-select label="Zahlungsart" v-model="invoiceData.billing_type" :options="billingTypeOptions" sm/>
|
||||
</div>
|
||||
</tt-card>
|
||||
<tt-card>
|
||||
@@ -125,100 +167,20 @@ Vue.component('manual-invoice-modal', {
|
||||
</tt-card>
|
||||
<tt-card>
|
||||
<template v-slot:header><h5><i class="fas fa-paragraph mr-2"></i>Texte</h5></template>
|
||||
<tt-textarea label="Schlusstext" v-model="invoiceData.closingText" rows="4"/>
|
||||
<tt-textarea label="Steuerhinweis (z.B. Reverse Charge)" v-model="invoiceData.taxText" rows="2"/>
|
||||
<tt-textarea label="Steuerhinweis (z.B. Reverse Charge)" v-model="invoiceData.tax_text" rows="2"/>
|
||||
</tt-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="invoice-preview-pane" v-show="isLargeScreen || showPreviewOnSmallScreen">
|
||||
<div class="invoice-preview-document">
|
||||
<div style="height: 50px; margin-bottom: 32px">
|
||||
<img alt="Xinon Logo" src="/assets/images/xinon-full.png" style="text-align:left;height: 85px;">
|
||||
</div>
|
||||
<table class="preview-header-table">
|
||||
<tr>
|
||||
<td class="customer-details">
|
||||
<div>{{ invoiceData.customer.company }}</div>
|
||||
<div>{{ invoiceData.customer.name }}</div>
|
||||
<div>{{ invoiceData.customer.street }}</div>
|
||||
<div>{{ invoiceData.customer.zip }} {{ invoiceData.customer.city }}</div>
|
||||
<div v-if="invoiceData.customer.country !== 'Österreich'">{{ invoiceData.customer.country }}</div>
|
||||
</td>
|
||||
<td class="invoice-details-cell">
|
||||
<table class="invoice-details-box">
|
||||
<tr><td>Kundennummer:</td><td>{{ selectedCustomerObject.customer_number || '-' }}</td></tr>
|
||||
<tr><td>Rechnungsnummer:</td><td>{{ invoiceData.invoiceNumber }}</td></tr>
|
||||
<tr><td>Belegdatum:</td><td>{{ formatDate(invoiceData.invoiceDate) }}</td></tr>
|
||||
<tr v-if="invoiceData.customer.uid"><td>Ihre UID:</td><td>{{ invoiceData.customer.uid }}</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="separator"></div>
|
||||
<div class="preview-main">
|
||||
<h2 style="text-align: center; color: #005384; font-size: 1.5rem; margin-bottom: 1.5rem;">Ihre Rechnung vom {{ formatDate(invoiceData.invoiceDate) }}</h2>
|
||||
<table class="positions-table">
|
||||
<thead>
|
||||
<tr class="uneven">
|
||||
<th style="text-align: left; padding-left: 4pt;">Leistung / Produkt</th>
|
||||
<th style="text-align: center;">Zeitraum</th>
|
||||
<th style="text-align: right;">Preis</th>
|
||||
<th style="text-align: center;">Menge</th>
|
||||
<th style="text-align: right;">Netto €</th>
|
||||
<th style="text-align: right;">Ust. %</th>
|
||||
<th style="text-align: right; padding-right: 4pt;">Brutto €</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(p, index) in invoiceData.positions">
|
||||
<tr :class="{'uneven': index % 2 === 0}"> <td style="vertical-align: top; padding-left: 4pt;">
|
||||
<strong>{{ p.product_name }}</strong>
|
||||
<div v-if="p.product_info" class="matchcode">{{ p.product_info }}</div>
|
||||
</td>
|
||||
<td style="text-align: center; vertical-align: top;">{{ formatPeriod(p.start_date, p.end_date) }}</td>
|
||||
<td style="text-align: right; vertical-align: top;">{{ formatPrice(p.price) }}</td>
|
||||
<td style="text-align: center; vertical-align: top;">{{ p.amount }}</td>
|
||||
<td style="text-align: right; vertical-align: top;">{{ formatPrice((p.amount || 0) * (p.price || 0)) }}</td>
|
||||
<td style="text-align: right; vertical-align: top;">{{ p.vatrate }}%</td>
|
||||
<td style="text-align: right; padding-right: 4pt; vertical-align: top;">{{ formatPrice(((p.amount || 0) * (p.price || 0)) * (1 + (p.vatrate || 0) / 100)) }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="totals-section">
|
||||
<table class="totals-table">
|
||||
<tr class="netto">
|
||||
<th>Gesamtbetrag Netto:</th>
|
||||
<td>{{ formatPrice(totals.net) }} €</td>
|
||||
</tr>
|
||||
<tr class="ust" v-for="(vatValue, vatRate) in totals.vat" :key="vatRate">
|
||||
<th>+ Umsatzsteuer {{ vatRate }}%:</th>
|
||||
<td>{{ formatPrice(vatValue) }} €</td>
|
||||
</tr>
|
||||
<tr class="brutto">
|
||||
<th>Gesamtbetrag Brutto:</th>
|
||||
<td>{{ formatPrice(totals.gross) }} €</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="payment-info">
|
||||
<p v-if="invoiceData.taxText" style="font-weight: bold;">{{invoiceData.taxText}}</p>
|
||||
Bitte <b>überweisen</b> Sie den Rechnungsbetrag bis zum <b>{{ formatDate(invoiceData.dueDate) }}</b> auf folgendes Konto:<br />
|
||||
<b style="padding-left: 4pt;">IBAN: {{ bankDetails.iban }}</b><br />
|
||||
<b style="padding-left: 4pt;">BIC: {{ bankDetails.bic }}</b><br /><br />
|
||||
Bitte geben Sie als Verwendungszweck unbedingt die Rechnungsnummer an.
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-footer">
|
||||
<div style="color:grey;text-align: center; width: 100%;">
|
||||
<span>XINON GmbH | Fladnitz 150 | 8322 Studenzen</span><br>
|
||||
<span>Tel.: +43 3115 40800 | E-Mail: office@xinon.at</span><br>
|
||||
<span>UID: ATU68711968 | FN: 416556h | LG: Feldbach</span><br>
|
||||
<span>IBAN: {{ bankDetails.iban }} | BIC: {{ bankDetails.bic }}</span><br>
|
||||
</div>
|
||||
<div class="page-number">Seite 1 von 1</div>
|
||||
<div class="pdf-preview-container">
|
||||
<div v-if="pdfLoading" class="pdf-loading">
|
||||
<i class="fas fa-spinner fa-spin fa-3x"></i>
|
||||
<p>PDF wird generiert...</p>
|
||||
</div>
|
||||
<object v-else :data="pdfPreviewUrl" type="application/pdf" width="100%" height="100%">
|
||||
<p>PDF Vorschau kann nicht angezeigt werden. <a :href="pdfPreviewUrl" target="_blank">Hier klicken zum Öffnen</a></p>
|
||||
</object>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,22 +192,36 @@ Vue.component('manual-invoice-modal', {
|
||||
selectedCustomerObject: {},
|
||||
isLargeScreen: window.innerWidth >= 1920,
|
||||
showPreviewOnSmallScreen: false,
|
||||
pdfLoading: false,
|
||||
pdfPreviewUrl: '',
|
||||
previewDebounceTimer: null,
|
||||
invoiceData: {
|
||||
id: null,
|
||||
invoiceNumber: `RE-${new Date().getFullYear()}-`,
|
||||
invoiceDate: moment().unix(),
|
||||
dueDate: moment().add(14, 'days').unix(),
|
||||
status: 'draft',
|
||||
billingAddressId: null,
|
||||
customer: { company: '', name: '', street: '', zip: '', city: '', country: 'Österreich', uid: '' },
|
||||
invoice_number: `MRN${new Date().getFullYear()}-X000001`,
|
||||
invoice_date: moment().unix(),
|
||||
billingaddress_id: null,
|
||||
owner_id: null,
|
||||
customer_number: 0,
|
||||
fibu_account_number: 0,
|
||||
company: '',
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
street: '',
|
||||
zip: '',
|
||||
city: '',
|
||||
country: 'Österreich',
|
||||
uid: '',
|
||||
email: '',
|
||||
billing_type: 'invoice',
|
||||
tax_text: '',
|
||||
positions: [],
|
||||
closingText: 'Wir danken für Ihren Auftrag und verbleiben mit freundlichen Grüßen,\nIhr Xinon Team',
|
||||
taxText: '',
|
||||
},
|
||||
bankDetails: {
|
||||
iban: 'ATXX XXXX XXXX XXXX XXXX',
|
||||
bic: 'XXXXXXXX'
|
||||
total: 0,
|
||||
total_gross: 0
|
||||
},
|
||||
billingTypeOptions: [
|
||||
{value: 'invoice', text: 'Rechnung'},
|
||||
{value: 'sepa', text: 'SEPA'}
|
||||
],
|
||||
positionsConfig: {
|
||||
fields: {
|
||||
product_name: { type: 'input', label: 'Bezeichnung' },
|
||||
@@ -289,10 +265,27 @@ Vue.component('manual-invoice-modal', {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'invoiceData.billingAddressId': {
|
||||
'invoiceData': {
|
||||
handler() {
|
||||
this.debouncedPreviewUpdate();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
'invoiceData.billingaddress_id': {
|
||||
async handler(newId) {
|
||||
if (!newId) {
|
||||
this.invoiceData.customer = { company: '', name: '', street: '', zip: '', city: '', country: 'Österreich', uid: '' };
|
||||
this.invoiceData.company = '';
|
||||
this.invoiceData.firstname = '';
|
||||
this.invoiceData.lastname = '';
|
||||
this.invoiceData.street = '';
|
||||
this.invoiceData.zip = '';
|
||||
this.invoiceData.city = '';
|
||||
this.invoiceData.country = 'Österreich';
|
||||
this.invoiceData.uid = '';
|
||||
this.invoiceData.email = '';
|
||||
this.invoiceData.customer_number = 0;
|
||||
this.invoiceData.fibu_account_number = 0;
|
||||
this.invoiceData.owner_id = 0;
|
||||
this.selectedCustomerObject = {};
|
||||
return;
|
||||
}
|
||||
@@ -300,15 +293,18 @@ Vue.component('manual-invoice-modal', {
|
||||
if (response.data.status === 'OK' && response.data.result.address) {
|
||||
const addr = response.data.result.address;
|
||||
this.selectedCustomerObject = addr;
|
||||
this.invoiceData.customer = {
|
||||
company: addr.company,
|
||||
name: `${addr.firstname} ${addr.lastname}`,
|
||||
street: addr.street,
|
||||
zip: addr.zip,
|
||||
city: addr.city,
|
||||
country: 'Österreich',
|
||||
uid: addr.uid
|
||||
};
|
||||
this.invoiceData.company = addr.company || '';
|
||||
this.invoiceData.firstname = addr.firstname || '';
|
||||
this.invoiceData.lastname = addr.lastname || '';
|
||||
this.invoiceData.street = addr.street || '';
|
||||
this.invoiceData.zip = addr.zip || '';
|
||||
this.invoiceData.city = addr.city || '';
|
||||
this.invoiceData.country = 'Österreich';
|
||||
this.invoiceData.uid = addr.uid || '';
|
||||
this.invoiceData.email = addr.email || '';
|
||||
this.invoiceData.customer_number = addr.customer_number || 0;
|
||||
this.invoiceData.fibu_account_number = addr.fibu_account_number || 0;
|
||||
this.invoiceData.owner_id = newId;
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
@@ -316,16 +312,10 @@ Vue.component('manual-invoice-modal', {
|
||||
},
|
||||
created() {
|
||||
if (this.initialData) {
|
||||
// FIX: Merge initial data with default structure to ensure all keys, especially nested ones, exist.
|
||||
this.invoiceData = {
|
||||
...this.invoiceData, // Start with default structure
|
||||
...JSON.parse(JSON.stringify(this.initialData)) // Overwrite with passed data
|
||||
...this.invoiceData,
|
||||
...JSON.parse(JSON.stringify(this.initialData))
|
||||
};
|
||||
// Explicitly ensure nested objects exist if they weren't in initialData
|
||||
if (!this.invoiceData.customer) {
|
||||
this.invoiceData.customer = { company: '', name: '', street: '', zip: '', city: '', country: 'Österreich', uid: '' };
|
||||
}
|
||||
// Ensure positions is an array
|
||||
if (!Array.isArray(this.invoiceData.positions)) {
|
||||
try {
|
||||
const parsed = JSON.parse(this.invoiceData.positions);
|
||||
@@ -338,35 +328,115 @@ Vue.component('manual-invoice-modal', {
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
window.addEventListener('keydown', this.handleGlobalKeydown);
|
||||
this.handleResize();
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.overlay) {
|
||||
this.$refs.overlay.focus();
|
||||
}
|
||||
this.updatePdfPreview();
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
window.removeEventListener('keydown', this.handleGlobalKeydown);
|
||||
if (this.previewDebounceTimer) {
|
||||
clearTimeout(this.previewDebounceTimer);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() { this.$emit('close'); },
|
||||
handleResize() { this.isLargeScreen = window.innerWidth >= 1920; },
|
||||
togglePreviewVisibility() { if (!this.isLargeScreen) this.showPreviewOnSmallScreen = !this.showPreviewOnSmallScreen; },
|
||||
formatPrice(value) { return new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value || 0); },
|
||||
formatDate(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
return moment.unix(timestamp).format('DD.MM.YYYY');
|
||||
},
|
||||
formatPeriod(start, end) {
|
||||
if (!start) return '';
|
||||
const startDate = moment(start);
|
||||
const endDate = end ? moment(end) : moment(start);
|
||||
if (!startDate.isValid()) return '';
|
||||
if (startDate.isSame(endDate, 'day')) return startDate.format('DD.MM.YYYY');
|
||||
if(startDate.isValid() && endDate.isValid()) {
|
||||
return `${startDate.format('DD.MM.YYYY')} - ${endDate.format('DD.MM.YYYY')}`;
|
||||
saveInvoice() {
|
||||
if (!this.invoiceData.billingaddress_id) {
|
||||
window.notify('error', 'Bitte wählen Sie einen Kunden aus.');
|
||||
return;
|
||||
}
|
||||
if (!this.invoiceData.positions || this.invoiceData.positions.length === 0) {
|
||||
window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
|
||||
return;
|
||||
}
|
||||
this.$emit('save', this.invoiceData);
|
||||
},
|
||||
handleResize() { this.isLargeScreen = window.innerWidth >= 1920; },
|
||||
handleGlobalKeydown(event) {
|
||||
// Handle CTRL+Q to toggle preview on small screens
|
||||
if (event.ctrlKey && event.key === 'q') {
|
||||
event.preventDefault();
|
||||
this.togglePreviewVisibility();
|
||||
}
|
||||
},
|
||||
togglePreviewVisibility() { if (!this.isLargeScreen) this.showPreviewOnSmallScreen = !this.showPreviewOnSmallScreen; },
|
||||
debouncedPreviewUpdate() {
|
||||
if (this.previewDebounceTimer) {
|
||||
clearTimeout(this.previewDebounceTimer);
|
||||
}
|
||||
this.previewDebounceTimer = setTimeout(() => {
|
||||
this.updatePdfPreview();
|
||||
}, 2000);
|
||||
},
|
||||
async updatePdfPreview() {
|
||||
this.pdfLoading = true;
|
||||
|
||||
try {
|
||||
// Calculate position totals
|
||||
const positions = this.invoiceData.positions.map(p => {
|
||||
const amount = parseFloat(p.amount) || 0;
|
||||
const price = parseFloat(p.price) || 0;
|
||||
const vatrate = parseFloat(p.vatrate) || 0;
|
||||
const price_total = amount * price;
|
||||
const price_gross = price_total * (1 + vatrate / 100);
|
||||
|
||||
return {
|
||||
...p,
|
||||
amount,
|
||||
price,
|
||||
vatrate,
|
||||
price_total,
|
||||
price_gross
|
||||
};
|
||||
});
|
||||
|
||||
const payload = {
|
||||
preview: true,
|
||||
invoice_number: this.invoiceData.invoice_number,
|
||||
invoice_date: this.invoiceData.invoice_date,
|
||||
customer_number: this.invoiceData.customer_number,
|
||||
fibu_account_number: this.invoiceData.fibu_account_number,
|
||||
company: this.invoiceData.company,
|
||||
firstname: this.invoiceData.firstname,
|
||||
lastname: this.invoiceData.lastname,
|
||||
street: this.invoiceData.street,
|
||||
zip: this.invoiceData.zip,
|
||||
city: this.invoiceData.city,
|
||||
country: this.invoiceData.country,
|
||||
email: this.invoiceData.email,
|
||||
uid: this.invoiceData.uid,
|
||||
tax_text: this.invoiceData.tax_text,
|
||||
billing_type: this.invoiceData.billing_type,
|
||||
total: this.totals.net,
|
||||
total_gross: this.totals.gross,
|
||||
positions: positions
|
||||
};
|
||||
|
||||
const response = await axios.post(
|
||||
`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/createPDF`,
|
||||
payload,
|
||||
{ responseType: 'blob' }
|
||||
);
|
||||
|
||||
// Create a blob URL for the PDF
|
||||
const blob = new Blob([response.data], { type: 'application/pdf' });
|
||||
if (this.pdfPreviewUrl) {
|
||||
URL.revokeObjectURL(this.pdfPreviewUrl);
|
||||
}
|
||||
this.pdfPreviewUrl = URL.createObjectURL(blob) + '#view=FitH';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF preview:', error);
|
||||
window.notify('error', 'Fehler beim Generieren der PDF-Vorschau');
|
||||
} finally {
|
||||
this.pdfLoading = false;
|
||||
}
|
||||
return startDate.format('DD.MM.YYYY');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user