diff --git a/application/ManualInvoice/ManualInvoiceController.php b/application/ManualInvoice/ManualInvoiceController.php new file mode 100644 index 000000000..91b019515 --- /dev/null +++ b/application/ManualInvoice/ManualInvoiceController.php @@ -0,0 +1,22 @@ + 'invoiceNumber', 'text' => 'Rechnungsnr.', 'table' => ['sortable' => true, 'filter' => 'search']], + ['key' => 'customerName', 'text' => 'Kunde', 'table' => ['sortable' => true, 'filter' => 'search']], + ['key' => 'invoiceDate', 'text' => 'Datum', 'table' => ['sortable' => true, 'filter' => 'date']], + ['key' => 'totalAmount', 'text' => 'Betrag', 'table' => ['sortable' => true, 'formatter' => 'formatPrice']], + ['key' => 'status', 'text' => 'Status', 'table' => ['filter' => 'select', 'filterOptions' => [ + ['value' => 'draft', 'text' => 'Entwurf'], + ['value' => 'sent', 'text' => 'Gesendet'], + ['value' => 'paid', 'text' => 'Bezahlt'], + ]]], + ['key' => 'actions', 'text' => 'Aktionen', 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]], + ]; + //@formatter:on +} \ No newline at end of file diff --git a/application/ManualInvoice/ManualInvoiceModel.php b/application/ManualInvoice/ManualInvoiceModel.php new file mode 100644 index 000000000..3f3eb170d --- /dev/null +++ b/application/ManualInvoice/ManualInvoiceModel.php @@ -0,0 +1,186 @@ + 1, 'invoiceNumber' => 'RE-2025-001', 'customerName' => 'Musterfirma GmbH', 'billingAddressId' => 1, + 'invoiceDate' => strtotime('2025-09-11'), 'dueDate' => strtotime('2025-09-25'), 'totalAmount' => 948.00, 'status' => 'paid', + 'positions' => json_encode([ + ['product_name' => 'IT-Support-Stunden', 'product_info' => 'Remote-Hilfe für Mitarbeiter', 'start_date' => '2025-09-10', 'end_date' => '2025-09-10', 'amount' => 2, 'price' => 120.00, 'vatrate' => 20], + ['product_name' => 'Netzwerk-Switch 24-Port', 'product_info' => 'Modell: XYZ-24G', 'start_date' => '2025-09-10', 'end_date' => '2025-09-10', 'amount' => 1, 'price' => 550.00, 'vatrate' => 20], + ]), + 'closingText' => 'Vielen Dank für Ihren Auftrag.', 'taxText' => '' + ], + [ + 'id' => 2, 'invoiceNumber' => 'RE-2025-002', 'customerName' => 'Beispiel AG', 'billingAddressId' => 2, + 'invoiceDate' => strtotime('2025-09-14'), 'dueDate' => strtotime('2025-09-28'), 'totalAmount' => 720.00, 'status' => 'sent', + 'positions' => json_encode([ + ['product_name' => 'Beratung Digitalisierungsstrategie', 'product_info' => 'Workshop am 05.09.2025', 'start_date' => '2025-09-05', 'end_date' => '2025-09-05', 'amount' => 4, 'price' => 150.00, 'vatrate' => 20], + ]), + 'closingText' => 'Wir freuen uns auf eine weiterhin gute Zusammenarbeit.', 'taxText' => '' + ], + [ + 'id' => 3, 'invoiceNumber' => 'RE-2025-003', 'customerName' => 'John Doe Services', 'billingAddressId' => 3, + 'invoiceDate' => strtotime('2025-09-16'), 'dueDate' => strtotime('2025-09-30'), 'totalAmount' => 912.00, 'status' => 'draft', + 'positions' => json_encode([ + ['product_name' => 'Kabelverlegung LWL', 'product_info' => 'Inhouse-Verkabelung Bürogebäude', 'start_date' => '2025-09-15', 'end_date' => '2025-09-15', 'amount' => 8, 'price' => 85.00, 'vatrate' => 20], + ['product_name' => 'LWL-Kabel 8 Fasern', 'product_info' => 'Pro Meter', 'start_date' => '2025-09-15', 'end_date' => '2025-09-15', 'amount' => 100, 'price' => 0.80, 'vatrate' => 20], + ]), + 'closingText' => 'Bei Fragen stehen wir Ihnen gerne zur Verfügung.', 'taxText' => '' + ], + [ + 'id' => 4, 'invoiceNumber' => 'RE-2025-004', 'customerName' => 'Bau & Co KG', 'billingAddressId' => 4, + 'invoiceDate' => strtotime('2025-09-06'), 'dueDate' => strtotime('2025-09-20'), 'totalAmount' => 1890.00, 'status' => 'paid', + 'positions' => json_encode([ + ['product_name' => 'Netzwerk-Grundinstallation Baustelle', 'product_info' => 'Containerdorf Einrichtung', 'start_date' => '2025-09-02', 'end_date' => '2025-09-02', 'amount' => 1, 'price' => 1200.00, 'vatrate' => 20], + ['product_name' => 'Stunden Elektriker', 'product_info' => 'Anpassungen Verteilerkasten', 'start_date' => '2025-09-02', 'end_date' => '2025-09-02', 'amount' => 5, 'price' => 75.00, 'vatrate' => 20], + ]), + 'closingText' => 'Vielen Dank für Ihren Auftrag.', 'taxText' => '' + ], + [ + 'id' => 5, 'invoiceNumber' => 'RE-2025-005', 'customerName' => 'Creative Solutions', 'billingAddressId' => 5, + 'invoiceDate' => strtotime('2025-09-15'), 'dueDate' => strtotime('2025-09-29'), 'totalAmount' => 1920.00, 'status' => 'sent', + 'positions' => json_encode([ + ['product_name' => 'Web-Entwicklung', 'product_info' => 'Umsetzung Landingpage "Herbst-Aktion"', 'start_date' => '2025-09-01', 'end_date' => '2025-09-12', 'amount' => 10, 'price' => 110.00, 'vatrate' => 20], + ['product_name' => 'Domain-Registrierung (.at)', 'product_info' => 'herbst-aktion.at', 'start_date' => '2025-09-01', 'end_date' => '2026-08-31', 'amount' => 1, 'price' => 500.00, 'vatrate' => 20], + ]), + 'closingText' => 'Wir freuen uns auf eine weiterhin gute Zusammenarbeit.', 'taxText' => '' + ], + [ + 'id' => 6, 'invoiceNumber' => 'RE-2025-006', 'customerName' => 'Logistik Express', 'billingAddressId' => 6, + 'invoiceDate' => strtotime('2025-08-28'), 'dueDate' => strtotime('2025-09-11'), 'totalAmount' => 3432.00, 'status' => 'paid', + 'positions' => json_encode([ + ['product_name' => 'Software-Lizenz WMS Pro', 'product_info' => 'Jahreslizenz für 10 User', 'start_date' => '2025-09-01', 'end_date' => '2026-08-31', 'amount' => 1, 'price' => 2500.00, 'vatrate' => 20], + ['product_name' => 'Mitarbeiterschulung WMS', 'product_info' => 'Vor Ort am 27.08.2025', 'start_date' => '2025-08-27', 'end_date' => '2025-08-27', 'amount' => 4, 'price' => 90.00, 'vatrate' => 20], + ]), + 'closingText' => 'Vielen Dank für Ihren Auftrag.', 'taxText' => '' + ], + [ + 'id' => 7, 'invoiceNumber' => 'RE-2025-007', 'customerName' => 'Gastro Profi', 'billingAddressId' => 7, + 'invoiceDate' => strtotime('2025-09-10'), 'dueDate' => strtotime('2025-09-24'), 'totalAmount' => 2577.60, 'status' => 'draft', + 'positions' => json_encode([ + ['product_name' => 'Kassensystem "GastroTouch"', 'product_info' => '2x Terminal, 1x Bondrucker', 'start_date' => '2025-09-09', 'end_date' => '2025-09-09', 'amount' => 2, 'price' => 899.00, 'vatrate' => 20], + ['product_name' => 'Installationspauschale', 'product_info' => 'Inkl. Einschulung', 'start_date' => '2025-09-09', 'end_date' => '2025-09-09', 'amount' => 1, 'price' => 350.00, 'vatrate' => 20], + ]), + 'closingText' => 'Bei Fragen stehen wir Ihnen gerne zur Verfügung.', 'taxText' => '' + ], + [ + 'id' => 8, 'invoiceNumber' => 'RE-2025-008', 'customerName' => 'Sicherheitsdienst Huber', 'billingAddressId' => 8, + 'invoiceDate' => strtotime('2025-09-01'), 'dueDate' => strtotime('2025-09-15'), 'totalAmount' => 1782.00, 'status' => 'sent', + 'positions' => json_encode([ + ['product_name' => 'IP Kamera 4K Dome', 'product_info' => 'Modell SEC-4K-D', 'start_date' => '2025-08-29', 'end_date' => '2025-08-29', 'amount' => 8, 'price' => 180.00, 'vatrate' => 20], + ['product_name' => 'Monatliche Wartungspauschale', 'product_info' => 'September 2025', 'start_date' => '2025-09-01', 'end_date' => '2025-09-30', 'amount' => 1, 'price' => 45.00, 'vatrate' => 20], + ]), + 'closingText' => 'Wir freuen uns auf eine weiterhin gute Zusammenarbeit.', 'taxText' => '' + ], + [ + 'id' => 9, 'invoiceNumber' => 'RE-2025-009', 'customerName' => 'Praxis Dr. Eder', 'billingAddressId' => 9, + 'invoiceDate' => strtotime('2025-09-12'), 'dueDate' => strtotime('2025-09-26'), 'totalAmount' => 3090.00, 'status' => 'draft', + 'positions' => json_encode([ + ['product_name' => 'Arbeitsstunden IT-Migration', 'product_info' => 'Serverumzug und Client-Setup', 'start_date' => '2025-09-11', 'end_date' => '2025-09-11', 'amount' => 5, 'price' => 95.00, 'vatrate' => 20], + ['product_name' => 'Server-Hardware "MedServ"', 'product_info' => 'Spez. für Arztpraxen', 'start_date' => '2025-09-11', 'end_date' => '2025-09-11', 'amount' => 1, 'price' => 1800.00, 'vatrate' => 20], + ['product_name' => 'Datensicherungslösung "CloudSafe"', 'product_info' => 'Einrichtungspauschale', 'start_date' => '2025-09-11', 'end_date' => '2025-09-11', 'amount' => 1, 'price' => 300.00, 'vatrate' => 20], + ]), + 'closingText' => 'Bei Fragen stehen wir Ihnen gerne zur Verfügung.', 'taxText' => '' + ], + [ + 'id' => 10, 'invoiceNumber' => 'RE-2025-010', 'customerName' => 'Architekturbüro Planweit', 'billingAddressId' => 10, + 'invoiceDate' => strtotime('2025-09-08'), 'dueDate' => strtotime('2025-09-22'), 'totalAmount' => 357.60, 'status' => 'paid', + 'positions' => json_encode([ + ['product_name' => 'Plotter Service', 'product_info' => 'Wartung und Reinigung', 'start_date' => '2025-09-04', 'end_date' => '2025-09-04', 'amount' => 1, 'price' => 250.00, 'vatrate' => 20], + ['product_name' => 'Netzwerkkabel Cat7', 'product_info' => 'Pro Meter', 'start_date' => '2025-09-04', 'end_date' => '2025-09-04', 'amount' => 40, 'price' => 1.20, 'vatrate' => 20], + ]), + 'closingText' => 'Vielen Dank für Ihren Auftrag.', 'taxText' => '' + ], +]; + +return $mockData; +} + + +class ManualInvoiceModel extends TTCrudBaseModel { + public int $id; + public ?string $invoiceNumber; + public ?int $invoiceDate; + public ?int $dueDate; + public int $billingAddressId; + public ?string $customerName; + public ?float $totalAmount; + public string $status; + public string $positions; + public string $closingText; + public string $taxText; + + private static function applyFilter(array $data, array $filter): array { + if (empty($filter)) { + return $data; + } + return array_filter($data, function ($row) use ($filter) { + foreach ($filter as $key => $value) { + if (!isset($row[$key]) || empty($value)) { + continue; + } + if (is_array($value)) { // Handle date ranges + if (isset($value['from']) && $row[$key] < $value['from']) return false; + if (isset($value['to']) && $row[$key] > $value['to']) return false; + } else if (is_array($row[$key])) { + if (!in_array($value, $row[$key])) return false; + } else if (stripos($row[$key], $value) === false) { + return false; + } + } + return true; + }); + } + + public static function getAll($filter = [], $limit = null, $offset = 0, $order = ["key" => null]): array + + { + $mockData = getMockData(); + $filteredData = self::applyFilter($mockData, $filter); + + if ($order['key'] !== null) { + usort($filteredData, function ($a, $b) use ($order) { + if ($a[$order['key']] == $b[$order['key']]) return 0; + if ($order['order'] === 'ASC') { + return $a[$order['key']] < $b[$order['key']] ? -1 : 1; + } else { + return $a[$order['key']] > $b[$order['key']] ? -1 : 1; + } + }); + } + + if ($limit !== null) { + return array_slice($filteredData, $offset, $limit); + } + return $filteredData; + } + + public static function count($filter = []): int { + $mockData = getMockData(); + return count(self::applyFilter($mockData, $filter)); + } + + public static function get($id) { + $mockData = getMockData(); + foreach ($mockData as $row) + if ($row['id'] == $id) + return new self($row); + return null; + } + + public static function create($data) { + error_log("ManualInvoiceModel::create called with: " . json_encode($data)); + return time(); + } + + public static function update($data) { + error_log("ManualInvoiceModel::update called with: " . json_encode($data)); + return 1; + } + + public static function delete($id) { + error_log("ManualInvoiceModel::delete called with ID: " . $id); + return 1; + } +} \ No newline at end of file diff --git a/public/js/pages/ManualInvoice/ManualInvoice.css b/public/js/pages/ManualInvoice/ManualInvoice.css new file mode 100644 index 000000000..4d2b28d3b --- /dev/null +++ b/public/js/pages/ManualInvoice/ManualInvoice.css @@ -0,0 +1,243 @@ +/* --- Main Overlay and Layout --- */ +.manual-invoice-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.6); + z-index: 1050; + display: flex; + overflow: hidden; +} + +.invoice-editor-pane, .invoice-preview-pane { + height: 100vh; + display: flex; + flex-direction: column; + transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1); +} + +.invoice-editor-pane { + flex: 0 0 50%; + background-color: #f4f5f7; + padding: 1rem; + overflow-y: hidden; +} + +.invoice-preview-pane { + flex: 1 1 auto; + background-color: #525659; + padding: 2rem; + overflow-y: auto; + display: flex; + justify-content: center; +} + +.info-bar { + position: absolute; + top: 0; + left: 0; + width: 100%; + background-color: rgba(0, 83, 132, 0.9); + color: white; + padding: 0.5rem; + text-align: center; + z-index: 1051; + font-size: 0.9rem; +} + +/* --- Responsive Layout & Toggle --- */ +@media (max-width: 1919px) { + .invoice-editor-pane, .invoice-preview-pane { + width: 100%; + flex-basis: 100%; + position: absolute; + } + + .manual-invoice-overlay.editor-active-small .invoice-preview-pane { + transform: translateX(100%); + } + + .manual-invoice-overlay.preview-active-small .invoice-editor-pane { + transform: translateX(-100%); + } + + .manual-invoice-overlay.preview-active-small .invoice-preview-pane { + transform: translateX(0); + } +} + +/* --- Editor Pane Specifics --- */ +.editor-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 1rem; + border-bottom: 1px solid #dee2e6; + flex-shrink: 0; +} + +.editor-header h3 { + margin: 0; +} + +.editor-actions { + display: flex; + gap: 0.5rem; +} + +.editor-content { + flex-grow: 1; + overflow-y: auto; + padding-top: 1rem; + padding-right: 10px; +} + +.editor-content .card { + margin-bottom: 1rem; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +/* --- Invoice Preview Styles (mimicking PDF) --- */ +.invoice-preview-document { + width: 210mm; + min-height: 297mm; + background-color: white; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); + font-family: "Open Sans", sans-serif, Verdana; + font-size: 12px; + display: flex; + flex-direction: column; + position: relative; +} + +.preview-header-table { + width: 100%; + border-collapse: collapse; + padding: 2cm 2cm 0 2cm; +} + +.customer-details { + vertical-align: bottom; + font-size: 14px; + padding-left: 30pt; + width: 65%; +} + +.invoice-details-cell { + vertical-align: bottom; +} + +.invoice-details-box { + border: 2px solid #e1e1e1; + padding: 6px; + font-size: 12px; +} + +.invoice-details-box table td { + padding: 2px 4px; +} + +.invoice-details-box table td:first-child { + text-align: right; + font-weight: bold; +} + +.separator { + margin: 24px 2cm 0 2cm; + height: 1px; + background-color: black; +} + +.preview-main { + padding: 1rem 2cm 0 2cm; + flex-grow: 1; +} + +.positions-table { + width: 100%; + border-collapse: collapse; + font-size: 11px; +} + +.positions-table th { + font-weight: bold; + border-bottom: 1px solid black; + padding: 8px 4px; + height: 28px; + vertical-align: middle; +} + +.positions-table td { + padding: 6px 4px; +} + +.positions-table tbody tr.uneven { + background-color: #ebebeb; +} + +.positions-table .matchcode { + padding-left: 12pt; + font-size: 10px; + color: #555; +} + +.totals-section { + margin-top: 1rem; + display: flex; + justify-content: flex-end; +} + +.totals-table { + width: 50%; + border-collapse: collapse; +} + +.totals-table td, .totals-table th { + padding: 4px; + text-align: left; +} + +.totals-table td:last-child { + text-align: right; +} + +.totals-table .netto { + font-weight: bold; + background-color: #ebebeb; + border-top: 1px solid black; + border-bottom: 1px solid black; +} + +.totals-table .ust { + font-size: 11px; + border-bottom: 1px solid #ddd; +} + +.totals-table .brutto { + font-weight: bold; + background-color: #ebebeb; + border-bottom: 3px double black; +} + +.payment-info { + margin-top: 20pt; +} + +.preview-footer { + padding: 1rem 2cm 2cm 2cm; + margin-top: auto; + border-top: 1px solid #e0e0e0; + font-size: 10px; + position: relative; +} + +.preview-footer .page-number { + text-align: right; +} \ No newline at end of file diff --git a/public/js/pages/ManualInvoice/ManualInvoice.js b/public/js/pages/ManualInvoice/ManualInvoice.js new file mode 100644 index 000000000..c21809295 --- /dev/null +++ b/public/js/pages/ManualInvoice/ManualInvoice.js @@ -0,0 +1,372 @@ +Vue.component('manual-invoice', { + template: ` + +
+ + +
+ + + + + + + +
+ `, + data() { + return { + isModalOpen: false, + 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; + this.isModalOpen = true; + }, + closeModal() { + this.isModalOpen = false; + 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(); + }, + 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(); + }, + formatPrice(value) { + return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value || 0); + }, + formatDate(timestamp) { + if (!timestamp) return '–'; + return moment.unix(timestamp).format('DD.MM.YYYY'); + } + } +}); + +Vue.component('manual-invoice-modal', { + props: ['initialData'], + template: ` +
+
+ Drücke STRG + Q um die Vorschau umzuschalten. +
+ +
+
+

{{ isCreateMode ? 'Neue Rechnung' : 'Rechnung bearbeiten' }}

+
+ + +
+
+
+ + + + + + +
+ + + +
+
+ + + + + + + + + +
+
+ +
+
+
+ Xinon Logo +
+ + + + + +
+
{{ invoiceData.customer.company }}
+
{{ invoiceData.customer.name }}
+
{{ invoiceData.customer.street }}
+
{{ invoiceData.customer.zip }} {{ invoiceData.customer.city }}
+
{{ invoiceData.customer.country }}
+
+ + + + + +
Kundennummer:{{ selectedCustomerObject.customer_number || '-' }}
Rechnungsnummer:{{ invoiceData.invoiceNumber }}
Belegdatum:{{ formatDate(invoiceData.invoiceDate) }}
Ihre UID:{{ invoiceData.customer.uid }}
+
+
+
+

Ihre Rechnung vom {{ formatDate(invoiceData.invoiceDate) }}

+ + + + + + + + + + + + + + + +
Leistung / ProduktZeitraumPreisMengeNetto €Ust. %Brutto €
+
+ + + + + + + + + + + + + +
Gesamtbetrag Netto:{{ formatPrice(totals.net) }} €
+ Umsatzsteuer {{ vatRate }}%:{{ formatPrice(vatValue) }} €
Gesamtbetrag Brutto:{{ formatPrice(totals.gross) }} €
+
+
+

{{invoiceData.taxText}}

+ Bitte überweisen Sie den Rechnungsbetrag bis zum {{ formatDate(invoiceData.dueDate) }} auf folgendes Konto:
+ IBAN: {{ bankDetails.iban }}
+ BIC: {{ bankDetails.bic }}

+ Bitte geben Sie als Verwendungszweck unbedingt die Rechnungsnummer an. +
+
+ +
+
+
+ `, + data() { + return { + isCreateMode: !this.initialData || !this.initialData.id, + customerApiUrl: window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress&fibu_primary_account=1', + selectedCustomerObject: {}, + isLargeScreen: window.innerWidth >= 1920, + showPreviewOnSmallScreen: false, + 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: '' }, + 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' + }, + positionsConfig: { + fields: { + product_name: { type: 'input', label: 'Bezeichnung' }, + product_info: { type: 'input', label: 'Zusatzinfo' }, + start_date: { type: 'input', label: 'Start', inputType: 'date' }, + end_date: { type: 'input', label: 'Ende', inputType: 'date' }, + amount: { type: 'input', label: 'Menge', inputType: 'number' }, + price: { type: 'input', label: 'Einzelpreis (€)', inputType: 'number' }, + vatrate: { type: 'input', label: 'USt. (%)', inputType: 'number' }, + }, + validateForm: (formData) => { + if (!formData.product_name) { window.notify('error', 'Bezeichnung ist erforderlich.'); return false; } + if (!formData.amount) { window.notify('error', 'Menge ist erforderlich.'); return false; } + if (formData.price === null || formData.price === undefined) { window.notify('error', 'Preis ist erforderlich.'); return false; } + return true; + } + } + }; + }, + computed: { + overlayClasses() { + return { + 'preview-active-small': !this.isLargeScreen && this.showPreviewOnSmallScreen, + 'editor-active-small': !this.isLargeScreen && !this.showPreviewOnSmallScreen, + }; + }, + totals() { + let net = 0; + const vat = {}; + if (!Array.isArray(this.invoiceData.positions)) return { net: 0, vat: {}, gross: 0 }; + + this.invoiceData.positions.forEach(p => { + const lineTotal = (parseFloat(p.amount) || 0) * (parseFloat(p.price) || 0); + const vatRate = parseInt(p.vatrate) || 0; + net += lineTotal; + if (!vat[vatRate]) { vat[vatRate] = 0; } + vat[vatRate] += lineTotal * (vatRate / 100); + }); + const gross = net + Object.values(vat).reduce((sum, v) => sum + v, 0); + return { net, vat, gross }; + } + }, + watch: { + 'invoiceData.billingAddressId': { + async handler(newId) { + if (!newId) { + this.invoiceData.customer = { company: '', name: '', street: '', zip: '', city: '', country: 'Österreich', uid: '' }; + this.selectedCustomerObject = {}; + return; + } + const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/Address/api?do=getAddress&id=${newId}`); + 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 + }; + } + }, + immediate: true + } + }, + 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 + }; + // 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); + this.invoiceData.positions = Array.isArray(parsed) ? parsed : []; + } catch (e) { + this.invoiceData.positions = []; + } + } + } + }, + mounted() { + window.addEventListener('resize', this.handleResize); + this.handleResize(); + this.$nextTick(() => { + if (this.$refs.overlay) { + this.$refs.overlay.focus(); + } + }); + }, + beforeDestroy() { + window.removeEventListener('resize', this.handleResize); + }, + 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')}`; + } + return startDate.format('DD.MM.YYYY'); + } + } +}); \ No newline at end of file