diff --git a/Layout/default/WarehouseOffer/PDF_MAIN.php b/Layout/default/WarehouseOffer/PDF_MAIN.php index aa6e85d49..478008f14 100644 --- a/Layout/default/WarehouseOffer/PDF_MAIN.php +++ b/Layout/default/WarehouseOffer/PDF_MAIN.php @@ -310,29 +310,69 @@ $formattedValidUntil = $validUntilDate ? date("d.m.Y", $validUntilDate) : date(" +totalDiscount) && $offer->totalDiscount > 0) { + $discountPercentage = $offer->totalDiscount; + // Calculate the monetary value of the discount + $discountAmount = ($subTotal * $discountPercentage) / 100; + // Calculate the subtotal after applying the discount + $subTotalAfterDiscount = $subTotal - $discountAmount; +} + +// Recalculate VAT and Grand Total based on the potentially discounted subtotal +if ($includeTax) { + // $vatRate should be a float, e.g., 0.20 for 20% + $vatAmount = $subTotalAfterDiscount * $vatRate; + $grandTotal = $subTotalAfterDiscount + $vatAmount; +} else { + $vatAmount = 0; // Ensure VAT is zero if tax is not included + $grandTotal = $subTotalAfterDiscount; +} + +// --- DISPLAY LOGIC --- +?> - 0): + // Define a label for the discount. You can add 'discount' to your $text array. + $discountLabel = $text['summary']['discount'] ?? 'Rabatt'; + ?> + + + + + + + - - - - - - - - - + + + + +
:
(%):-
:
:
:
:
diff --git a/application/WarehouseOffer/WarehouseOfferModel.php b/application/WarehouseOffer/WarehouseOfferModel.php index ace28e733..cbfa931e4 100644 --- a/application/WarehouseOffer/WarehouseOfferModel.php +++ b/application/WarehouseOffer/WarehouseOfferModel.php @@ -6,7 +6,7 @@ class WarehouseOfferModel extends TTCrudBaseModel { public string $reference; public string $customerNumber; public string $customerName; - public string $contactPerson; + public ?string $contactPerson; public string $customerStreet; public string $customerCity; public string $customerZip; diff --git a/application/WarehouseOrder/WarehouseOrderController.php b/application/WarehouseOrder/WarehouseOrderController.php index 55c5bd7c7..0e4d9d939 100644 --- a/application/WarehouseOrder/WarehouseOrderController.php +++ b/application/WarehouseOrder/WarehouseOrderController.php @@ -345,31 +345,80 @@ $appendToBody return; } + $order = WarehouseOrderModel::get($postData['orderId']); + $orderAsArray = (array) $order; + + $fullLogMessage = ''; + + // 1. Status change message + if ($postData['status'] !== 'noChanges') { + $statusColumn = array_values(array_filter($this->columns, fn($c) => $c['key'] === 'status'))[0]['modal']['items']; + + $oldStatusKey = array_search($order->status, array_column($statusColumn, 'value')); + $newStatusKey = array_search($postData['status'], array_column($statusColumn, 'value')); + + $oldStatusText = ($oldStatusKey !== false) ? $statusColumn[$oldStatusKey]['text'] : $order->status; + $newStatusText = ($newStatusKey !== false) ? $statusColumn[$newStatusKey]['text'] : $postData['status']; + + $fullLogMessage .= 'Status wurde geändert von ' . $oldStatusText . ' auf ' . $newStatusText . '.'; + } + + // 2. Main note from user + if (!empty($postData['note'])) { + $fullLogMessage .= ($fullLogMessage ? "\n\n" : "") . $postData['note']; + } + + // 3. Handle delivery data if present + if (isset($postData['deliveryData']) && is_array($postData['deliveryData'])) { + $deliveryDetails = []; + + foreach ($postData['deliveryData'] as $delivery) { + $orderedAmount = floatval($delivery['orderedAmount']); + $deliveredAmount = floatval($delivery['amount']); + $articleName = $delivery['articleName']; + + // Only log if there's a discrepancy + if ($deliveredAmount < $orderedAmount) { + $reasonText = !empty($delivery['reason']) ? " Grund: " . $delivery['reason'] . "." : ""; + $discrepancyMessage = "Artikel '$articleName': {$deliveredAmount} von {$orderedAmount} Stk. geliefert.{$reasonText}"; + + if (isset($delivery['cancelRest']) && $delivery['cancelRest']) { + $remaining = $orderedAmount - $deliveredAmount; + $discrepancyMessage .= " Restliche {$remaining} Stk. storniert."; + } + $deliveryDetails[] = $discrepancyMessage; + } + } + + if (!empty($deliveryDetails)) { + $fullLogMessage .= ($fullLogMessage ? "\n\n" : "") . "Lieferdetails:\n- " . implode("\n- ", $deliveryDetails); + } + } + $log = [ "table" => "WarehouseOrder", "rowId" => intval($postData['orderId']), "type" => $postData['status'] === 'noChanges' ? 'noChanges' : 'statusChange', "fileIds" => $postData['fileIds'] ?? null, - "message" => $postData['note'] ?? null, + "message" => trim($fullLogMessage), "createBy" => intval($this->user->id), "create" => time() ]; try { - $order = WarehouseOrderModel::get($log['rowId']); if ($postData['status'] !== 'noChanges') { - $oldStatusText = array_values(array_filter($this->columns, fn($c) => $c['key'] === 'status'))[0]['modal']['items'][array_search($order->status, array_column(array_values(array_filter($this->columns, fn($c) => $c['key'] === 'status'))[0]['modal']['items'], 'value'))]['text']; - $newStatusText = array_values(array_filter($this->columns, fn($c) => $c['key'] === 'status'))[0]['modal']['items'][array_search($postData['status'], array_column(array_values(array_filter($this->columns, fn($c) => $c['key'] === 'status'))[0]['modal']['items'], 'value'))]['text']; - $log['message'] = 'Status wurde geändert von ' . $oldStatusText . ' auf ' . $newStatusText . ($log['message'] ? ': ' . $log['message'] : ''); - $order->status = $postData['status']; - $order = (array) $order; - WarehouseOrderModel::update($order); + $orderAsArray['status'] = $postData['status']; + WarehouseOrderModel::update($orderAsArray); } - WarehouseLogModel::create($log); - self::returnJson(['success' => 'Log entry created']); + // Only create a log entry if there's actually something to log + if ($postData['status'] !== 'noChanges' || !empty($log['message']) || !empty($log['fileIds'])) { + WarehouseLogModel::create($log); + } + + self::returnJson(['success' => true, 'message' => 'Log entry created']); } catch (Exception $e) { - self::returnJson(['error' => 'Error creating log entry']); + self::returnJson(['error' => 'Error creating log entry: ' . $e->getMessage()]); } } diff --git a/public/js/pages/WarehouseOffer/WarehouseOffer.js b/public/js/pages/WarehouseOffer/WarehouseOffer.js index fa7d86e7b..d2006dcd9 100644 --- a/public/js/pages/WarehouseOffer/WarehouseOffer.js +++ b/public/js/pages/WarehouseOffer/WarehouseOffer.js @@ -638,7 +638,7 @@ Vue.component('warehouse-offer-modal', { label: 'Artikel', customFieldReference: 'WarehouseArticle', }, - amount: {type: 'input', label: 'Menge', inputType: 'number'}, + amount: {type: 'input', label: 'Menge', inputType: 'number', editableInTable: true}, unit: {type: 'input', label: 'Einheit'}, articleNumber: {type: 'input', label: 'Artikelnummer'}, isAlternative: {type: 'checkbox', label: 'Alternativposition'}, diff --git a/public/js/pages/WarehouseOrder/WarehouseOrder.js b/public/js/pages/WarehouseOrder/WarehouseOrder.js index 34ba45896..501439c38 100644 --- a/public/js/pages/WarehouseOrder/WarehouseOrder.js +++ b/public/js/pages/WarehouseOrder/WarehouseOrder.js @@ -14,20 +14,34 @@ Vue.component('change-status-modal', { sendEmail: false, sendEmailViewedPDF: false, sendEmailMail: '', - submitLoading: false + submitLoading: false, + deliveredPositions: {} // To track delivery details for each position }; }, async mounted() { const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.orderId}}); - // if order.status is canceled emit close event and window.notify('error', 'Bestellung wurde storniert') if (response.data.status === 'cancelled') { this.$emit('close'); window.notify('error', 'Bestellung wurde storniert'); } this.order = response.data; + + // Initialize deliveredPositions after fetching the order + if (this.order && this.order.positions) { + this.order.positions.forEach((pos, index) => { + this.$set(this.deliveredPositions, index, { + amount: pos.amount, // Default delivered amount to the ordered amount + reason: '', + cancelRest: false, + articleName: pos.articleName, // Store for easy access in the submit method + orderedAmount: pos.amount + }); + }); + } }, computed: { availableStatuses() { + // This computed property remains unchanged switch (this.order.status) { case 'new': return [ @@ -73,6 +87,7 @@ Vue.component('change-status-modal', { }, methods: { async handleFileUpload(event) { + // This method remains unchanged const files = event.target.files; if (!files.length) return; @@ -101,11 +116,11 @@ Vue.component('change-status-modal', { window.notify('error', `Error uploading file "${file.name}"`); } } - - // Clear the file input event.target.value = ''; }, - removeFile: index => this.uploadedFiles.splice(index, 1), + removeFile(index) { + this.uploadedFiles.splice(index, 1) + }, async submit() { this.submitLoading = true; if (this.newStatus === 'accepted' && this.sendEmail && !this.sendEmailMail) { @@ -124,11 +139,19 @@ Vue.component('change-status-modal', { } const fileIds = this.uploadedFiles.map(file => file.id); + + // Prepare delivery data if the status is related to delivery + let deliveryData = null; + if (this.newStatus === 'partiallyDelivered' || this.newStatus === 'fullyDelivered') { + deliveryData = this.deliveredPositions; + } + const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/createNewLogAction`, { orderId: this.order.id, status: this.newStatus, note: this.note, fileIds: JSON.stringify(fileIds), + deliveryData: deliveryData // Send the new delivery data to the backend }); if (response.data.success) { @@ -141,6 +164,7 @@ Vue.component('change-status-modal', { this.submitLoading = false; }, async generateShippingNote() { + // This method remains unchanged const positions = this.order.positions.map(position => ({ article: position.article, amount: position.amount, @@ -165,6 +189,7 @@ Vue.component('change-status-modal', { return response.data.id; }, async submitEmail(shippingNote = null) { + // This method remains unchanged const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/sendEmail?id=${this.order.id}&email=${this.sendEmailMail}${shippingNote ? '&shippingNote=' + shippingNote : ''}`); if (response.data.success) { @@ -175,82 +200,111 @@ Vue.component('change-status-modal', { } }, template: ` - - - + + ` }); +// The other components in WarehouseOrder.js (warehouse-order-modal, tt-file, etc.) remain unchanged. +// I am including them here to provide the full file content. Vue.component('warehouse-order-modal', { props: { id: {type: [String, Number], required: true}, @@ -527,48 +581,46 @@ Vue.component('tt-file', { Vue.component('warehouse-order-detail', { template: ` - - - - + + `, props: ['id'], data: () => ({order: {}, orderLog: null, loading: true}), async mounted() { @@ -600,7 +652,8 @@ Vue.component('warehouse-order', { - + + `, data: () => ({ orderModalId: null, @@ -619,4 +672,4 @@ Vue.component('warehouse-order', { calculateSum: positions => positions.reduce((sum, {amount, buyPrice}) => sum + amount * buyPrice, 0), openPDF: order => window.open(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/createPDF?id=${order.id}`) } -}); +}); \ No newline at end of file diff --git a/public/plugins/vue/tt-components/tt-position-manager.js b/public/plugins/vue/tt-components/tt-position-manager.js index 42ffff1c5..50d676a3c 100644 --- a/public/plugins/vue/tt-components/tt-position-manager.js +++ b/public/plugins/vue/tt-components/tt-position-manager.js @@ -4,18 +4,13 @@ Vue.component('tt-resolver', { reference: { type: String, required: true }, autocomplete: { type: Boolean, default: false } }, - data: () => ({ - loading: true, - text: '' - }), - template: ` -
-
- Loading... -
+ data: () => ({loading: true, text: ''}), + template: `
+
+ Loading...
- {{ text }} - `, +
+ {{ text }}`, async created() { const cacheKey = `${this.reference}_${this.value}_${this.autocomplete}`; const cached = JSON.parse(localStorage.getItem(cacheKey) || 'null'); @@ -40,360 +35,417 @@ Vue.component('tt-resolver', { } }); - -Vue.component('tt-positions-manager', - { - props: { - value: {type: [Array, String], required: false}, - config: {type: Object, required: true}, - groupMode: {type: Boolean, default: false}, - submitLoading: {type: Boolean, default: false} - }, - data() { - return { - window: window, - positions: this.value, - formData: {}, - groupName: '', - selectedIndex: null, - editingGroupName: null, - tempGroupName: '', - } - }, - template: ` -
- -
- -
- -
- - -
- - -
- - -
- - - - - - - - - - - - - - -
{{ field.label }}Aktionen
-
- `, - methods: { - updateField(key, value) { - this.$set(this.formData, key, value); - }, - defaultValidateForm(formData) { - for (const field of this.config["validateFormOptions"]) { - if (!(formData[field.key] || formData[field.key + '_text'])) { - window.notify('error', field.message); - return false; - } - } - return true; - }, - checkEmitDisplayValueAutocomplete() { - for (const [key, field] of Object.entries(this.config.fields)) { - if ((typeof field.showCondition === 'function' && field.showCondition(this.formData) === true || !field.showCondition) && field.type === 'autocomplete' && field.emitDisplayValue && (isNaN(this.formData[key]) || !this.formData[key]) && this.$refs['autocomplete-' + key][0]) { - this.$set(this.formData, key + '_text', this.$refs['autocomplete-' + key][0].displayValue); - this.$delete(this.formData, key); - } - - if ((typeof field.showCondition === 'function' && field.showCondition(this.formData) === true || !field.showCondition) && field.type === 'input-article' && field.emitDisplayValue && (isNaN(this.formData[key]) || !this.formData[key]) && this.$refs['article-' + key][0]) { - console.log(this.$refs['article-' + key][0].$refs.autocomplete); - this.$set(this.formData, key + '_text', this.$refs['article-' + key][0].$refs.autocomplete.displayValue); - this.$delete(this.formData, key); - } - - if (field.emitDisplayValue && this.formData[key] !== null && this.formData[key] !== undefined) { - this.$delete(this.formData, key + '_text'); - } - - } - }, - async saveEntry() { - this.checkEmitDisplayValueAutocomplete(); - if (this.config.hasOwnProperty('validateFormOptions') && !this.defaultValidateForm(this.formData)) return; - else if (this.config.validateForm && !await this.config.validateForm(this.formData)) return; - - if (this.selectedIndex === null) this.positions.push(this.formData); - else this.$set(this.positions, this.selectedIndex, this.formData); - - if (this.config.customOrdering) { - this.positions.sort((a, b) => a[this.config.customOrdering] - b[this.config.customOrdering]); - } - - this.$emit('input', this.positions); - this.resetForm(); - }, - addGroup() { - this.positions.push({_group: this.groupName}); - this.groupName = ''; - }, - startGroupEdit(groupName) { - this.editingGroupName = groupName; - this.tempGroupName = groupName; - }, - saveGroupName(oldGroupName) { - if (this.tempGroupName.trim() === '') { - window.notify('error', 'Gruppenname darf nicht leer sein'); - return; - } - - if (this.tempGroupName !== oldGroupName) { - // Update all positions with the old group name to the new group name - this.positions.forEach(position => { - if (position._group === oldGroupName) { - this.$set(position, '_group', this.tempGroupName); - } - }); - this.$emit('input', this.positions); - } - - this.editingGroupName = null; - this.tempGroupName = ''; - }, - cancelGroupEdit() { - this.editingGroupName = null; - this.tempGroupName = ''; - }, - deleteGroup(groupName) { - if (confirm(`Möchten Sie die Gruppe "${groupName}" wirklich löschen? Alle Einträge werden in "Keine Gruppe" verschoben.`)) { - // Move all items from this group to "Keine Gruppe" and remove empty group entries - this.positions = this.positions.filter(position => { - if (position._group === groupName) { - // If it's just a group header (only has _group property), remove it - if (Object.keys(position).length === 1) { - return false; - } - // Otherwise, move to "Keine Gruppe" - this.$set(position, '_group', 'Keine Gruppe'); - } - return true; - }); - this.$emit('input', this.positions); - } - }, - getActualIndex(position, groupName, groupIndex) { - // Find the actual index in the positions array - if (!this.groupMode) return groupIndex; - - let actualIndex = 0; - let currentGroupIndex = 0; - - for (let i = 0; i < this.positions.length; i++) { - const pos = this.positions[i]; - const posGroup = pos._group ?? 'Keine Gruppe'; - - if (posGroup === groupName && Object.keys(pos).length > 1) { - if (currentGroupIndex === groupIndex) { - return i; - } - currentGroupIndex++; - } - } - - return groupIndex; // Fallback - }, - async editEntry(index) { - this.selectedIndex = index; - for (const [key, field] of Object.entries(this.config.fields)) { - if (field.type === 'autocomplete' && field.emitDisplayValue) { - await this.$nextTick(); - if (this.positions[index][key + '_text'] && this.$refs['autocomplete-' + key][0]) { - console.log('inhere'); - this.$refs['autocomplete-' + key][0].displayValue = this.positions[index][key + '_text']; - this.$set(this.formData, key, this.positions[index][key]); - } - } - } - this.formData = {...this.positions[index]}; - }, - deleteEntry(index) { - this.positions.splice(index, 1); - this.$emit('input', this.positions); - }, - resetForm() { - this.formData = {}; - for (const [key, field] of Object.entries(this.config.fields)) { - if ( - typeof field.showCondition === 'function' && field.showCondition(this.formData) === true && - field.type === 'autocomplete' && field.emitDisplayValue && this.$refs['autocomplete-' + key][0]) { - this.$refs['autocomplete-' + key][0].displayValue = ''; - } - if (field.inputType === 'date') { - this.$set(this.formData, key, moment().format('YYYY-MM-DD')); - } - } - this.selectedIndex = null; - }, - formatFieldValue(value, field) { - if (field.formatter) return field.formatter(value); - if (field.inputType === 'date') return moment(value).format('DD.MM.YYYY'); - return value; - }, - }, - //TODO: cleanup - created() { - if (this.config["customMethods"]) Object.assign(this, this.config.customMethods); - if (!this.positions) this.positions = []; - if (typeof this.positions === 'string') { - try { - this.positions = JSON.parse(this.positions); - } catch (e) { - console.error(e); - this.positions = []; - } - } - }, - mounted() { - this.resetForm(); - }, - computed: { - groupedPositions() { - const groups = {}; - for (const position of this.positions) { - const group = position._group ?? 'Keine Gruppe'; - if (!groups[group]) groups[group] = []; - if (Object.keys(position).length !== 1) groups[group].push(position); - } - return groups; - }, - positionsToRender() { - return this.groupMode ? this.groupedPositions : {'': this.positions}; - }, - allGroups() { - return Object.keys(this.groupedPositions); - } - }, - watch: { - value: { - handler() { - this.positions = this.value; - }, - deep: true +Vue.component('tt-positions-manager', { + props: { + value: {type: [Array, String], required: false}, + config: {type: Object, required: true}, + groupMode: {type: Boolean, default: false}, + submitLoading: {type: Boolean, default: false} + }, + data() { + return { + window: window, + positions: this.value, + formData: {}, + groupName: '', + selectedIndex: null, + editingGroupName: null, + tempGroupName: '', + // New state for instant editing + editingCell: { + groupName: null, // For grouped positions + index: null, // Index within the group or flat array + key: null // Field key being edited } } - }) + }, + template: ` +
+ +
+ +
+ +
+ + +
+ + +
+ + +
+ + + + + + + + + + + + + + +
{{ field.label }}Aktionen
+
+ `, + methods: { + updateField(key, value) { + this.$set(this.formData, key, value); + }, + defaultValidateForm(formData) { + for (const field of this.config["validateFormOptions"]) { + if (!(formData[field.key] || formData[field.key + '_text'])) { + window.notify('error', field.message); + return false; + } + } + return true; + }, + checkEmitDisplayValueAutocomplete() { + for (const [key, field] of Object.entries(this.config.fields)) { + if ((typeof field.showCondition === 'function' && field.showCondition(this.formData) === true || !field.showCondition) && field.type === 'autocomplete' && field.emitDisplayValue && (isNaN(this.formData[key]) || !this.formData[key]) && this.$refs['autocomplete-' + key][0]) { + this.$set(this.formData, key + '_text', this.$refs['autocomplete-' + key][0].displayValue); + this.$delete(this.formData, key); + } + + if ((typeof field.showCondition === 'function' && field.showCondition(this.formData) === true || !field.showCondition) && field.type === 'input-article' && field.emitDisplayValue && (isNaN(this.formData[key]) || !this.formData[key]) && this.$refs['article-' + key][0]) { + console.log(this.$refs['article-' + key][0].$refs.autocomplete); + this.$set(this.formData, key + '_text', this.$refs['article-' + key][0].$refs.autocomplete.displayValue); + this.$delete(this.formData, key); + } + + if (field.emitDisplayValue && this.formData[key] !== null && this.formData[key] !== undefined) { + this.$delete(this.formData, key + '_text'); + } + + } + }, + async saveEntry() { + this.checkEmitDisplayValueAutocomplete(); + if (this.config.hasOwnProperty('validateFormOptions') && !this.defaultValidateForm(this.formData)) return; + else if (this.config.validateForm && !await this.config.validateForm(this.formData)) return; + + if (this.selectedIndex === null) this.positions.push(this.formData); + else this.$set(this.positions, this.selectedIndex, this.formData); + + if (this.config.customOrdering) { + this.positions.sort((a, b) => a[this.config.customOrdering] - b[this.config.customOrdering]); + } + + this.$emit('input', this.positions); + this.resetForm(); + }, + addGroup() { + this.positions.push({_group: this.groupName}); + this.groupName = ''; + }, + startGroupEdit(groupName) { + this.editingGroupName = groupName; + this.tempGroupName = groupName; + }, + saveGroupName(oldGroupName) { + if (this.tempGroupName.trim() === '') { + window.notify('error', 'Gruppenname darf nicht leer sein'); + return; + } + + if (this.tempGroupName !== oldGroupName) { + // Update all positions with the old group name to the new group name + this.positions.forEach(position => { + if (position._group === oldGroupName) { + this.$set(position, '_group', this.tempGroupName); + } + }); + this.$emit('input', this.positions); + } + + this.editingGroupName = null; + this.tempGroupName = ''; + }, + cancelGroupEdit() { + this.editingGroupName = null; + this.tempGroupName = ''; + }, + deleteGroup(groupName) { + if (confirm(`Möchten Sie die Gruppe "${groupName}" wirklich löschen? Alle Einträge werden in "Keine Gruppe" verschoben.`)) { + // Move all items from this group to "Keine Gruppe" and remove empty group entries + this.positions = this.positions.filter(position => { + if (position._group === groupName) { + // If it's just a group header (only has _group property), remove it + if (Object.keys(position).length === 1) { + return false; + } + // Otherwise, move to "Keine Gruppe" + this.$set(position, '_group', 'Keine Gruppe'); + } + return true; + }); + this.$emit('input', this.positions); + } + }, + getActualIndex(position, groupName, groupIndex) { + // Find the actual index in the positions array + if (!this.groupMode) return groupIndex; + + let actualIndex = 0; + let currentGroupIndex = 0; + + for (let i = 0; i < this.positions.length; i++) { + const pos = this.positions[i]; + const posGroup = pos._group ?? 'Keine Gruppe'; + + if (posGroup === groupName && Object.keys(pos).length > 1) { + if (currentGroupIndex === groupIndex) { + return i; + } + currentGroupIndex++; + } + } + + return groupIndex; // Fallback + }, + async editEntry(index) { + this.selectedIndex = index; + for (const [key, field] of Object.entries(this.config.fields)) { + if (field.type === 'autocomplete' && field.emitDisplayValue) { + await this.$nextTick(); + if (this.positions[index][key + '_text'] && this.$refs['autocomplete-' + key][0]) { + console.log('inhere'); + this.$refs['autocomplete-' + key][0].displayValue = this.positions[index][key + '_text']; + this.$set(this.formData, key, this.positions[index][key]); + } + } + } + this.formData = {...this.positions[index]}; + + // Scroll to the form container + this.$nextTick(() => { + this.$refs.formContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + }, + deleteEntry(index) { + this.positions.splice(index, 1); + this.$emit('input', this.positions); + }, + resetForm() { + this.formData = {}; + for (const [key, field] of Object.entries(this.config.fields)) { + if ( + typeof field.showCondition === 'function' && field.showCondition(this.formData) === true && + field.type === 'autocomplete' && field.emitDisplayValue && this.$refs['autocomplete-' + key][0]) { + this.$refs['autocomplete-' + key][0].displayValue = ''; + } + if (field.inputType === 'date') { + this.$set(this.formData, key, moment().format('YYYY-MM-DD')); + } + } + this.selectedIndex = null; + }, + formatFieldValue(value, field) { + if (field.formatter) return field.formatter(value); + if (field.inputType === 'date') return moment(value).format('DD.MM.YYYY'); + return value; + }, + // New methods for instant editing + startCellEdit(groupName, index, key, field) { + if (!field.editableInTable || !(field.type === 'input' || field.type === 'textarea')) return; + this.editingCell = { groupName, index, key }; + this.$nextTick(() => { + const inputElement = this.$el.querySelector(`.editable-cell input[type="${field.type}"], .editable-cell textarea`); + if (inputElement) { + inputElement.focus(); + } + }); + }, + saveCellEdit(position, key, groupName, index) { + this.editingCell = { groupName: null, index: null, key: null }; + // Ensure data is updated in the original positions array + const actualIndex = this.getActualIndex(position, groupName, index); + this.$set(this.positions, actualIndex, position); + this.$emit('input', this.positions); + }, + isCellEditing(groupName, index, key) { + return this.editingCell.groupName === groupName && + this.editingCell.index === index && + this.editingCell.key === key; + } + }, + //TODO: cleanup + created() { + if (this.config["customMethods"]) Object.assign(this, this.config.customMethods); + if (!this.positions) this.positions = []; + if (typeof this.positions === 'string') { + try { + this.positions = JSON.parse(this.positions); + } catch (e) { + console.error(e); + this.positions = []; + } + } + }, + mounted() { + this.resetForm(); + }, + computed: { + groupedPositions() { + const groups = {}; + for (const position of this.positions) { + const group = position._group ?? 'Keine Gruppe'; + if (!groups[group]) groups[group] = []; + if (Object.keys(position).length !== 1) groups[group].push(position); + } + return groups; + }, + positionsToRender() { + return this.groupMode ? this.groupedPositions : {'': this.positions}; + }, + allGroups() { + return Object.keys(this.groupedPositions); + } + }, + watch: { + value: { + handler() { + this.positions = this.value; + }, + deep: true + } + } +}); \ No newline at end of file