window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [ { "key": "openHistory", "title": "Historie", "class": "fas fa-history text-info", "condition": (row) => window.TT_CONFIG.ASSET_ADMIN === '1', }, { "key": "reserve", "title": "Reservieren", "class": "fas fa-calendar-alt text-warning", "condition": (row) => window.TT_CONFIG.ASSET_ADMIN === '1', }, { "key": "print", "title": "Label drucken", "class": "fas fa-print text-secondary", "condition": (row) => window.TT_CONFIG.ASSET_ADMIN === '1', } ]; // ================================================================================= // Main Asset Management Component // ================================================================================= Vue.component('asset-management', { template: ` `, data() { return { window: window, modalId: null, journalModalAssetId: null, reservationModalAsset: null, printModalAsset: null, crudConfig: window.TT_CONFIG.CRUD_CONFIG, } }, methods: { formatDate(timestamp, format) { if (!timestamp) return ''; return window.moment.unix(timestamp).format(format); }, isDatePast(timestamp) { if (!timestamp) return false; return window.moment.unix(timestamp).isBefore(window.moment(), 'day'); }, async updateTableWithoutModalsOpening() { if (this.$refs.table && this.$refs.table.$refs.table) { this.$refs.table.$refs.table.$set(this.$refs.table.$refs.table, 'rows', []); await this.$nextTick(); await this.$refs.table.$refs.table.refreshTable(); } } } }); // ================================================================================= // Asset Print Label Modal // ================================================================================= Vue.component('asset-print-label-modal', { props: { asset: { type: Object, required: true } }, template: `

Wählen Sie die gewünschte Label-Größe:

`, methods: { printLabel(size) { const url = window.TT_CONFIG.BASE_PATH + "/AssetManagement/printLabel?id=" + this.asset.id + "&size=" +size; window.open(url, '_blank'); this.$emit('close'); } } }); // ================================================================================= // Asset Image Component // ================================================================================= Vue.component('tt-asset-image', { props: { imageId: Number, allImageIds: { type: Array, default: () => [] } }, data() { return { isFullScreen: false, currentFullScreenImageId: null, }; }, template: `
`, methods: { onImageError(event) { event.target.src = 'https://placehold.co/60x60/eee/ccc?text=Error'; // Fallback placeholder }, openFullScreen(initialImageId) { if (this.imageId) { // Only open if an image exists this.isFullScreen = true; this.currentFullScreenImageId = initialImageId; this.$nextTick(() => { if (this.$refs.fullScreenOverlay) { this.$refs.fullScreenOverlay.focus(); // Focus the overlay to capture keydown events } document.addEventListener('keydown', this.handleKeyDown); }); } }, closeFullScreen() { this.isFullScreen = false; this.currentFullScreenImageId = null; document.removeEventListener('keydown', this.handleKeyDown); }, setFullScreenImage(imageId) { this.currentFullScreenImageId = imageId; }, prevImage() { const currentIndex = this.allImageIds.indexOf(this.currentFullScreenImageId); const prevIndex = (currentIndex - 1 + this.allImageIds.length) % this.allImageIds.length; this.currentFullScreenImageId = this.allImageIds[prevIndex]; }, nextImage() { const currentIndex = this.allImageIds.indexOf(this.currentFullScreenImageId); const nextIndex = (currentIndex + 1) % this.allImageIds.length; this.currentFullScreenImageId = this.allImageIds[nextIndex]; }, handleKeyDown(event) { if (event.key === 'ArrowLeft') { this.prevImage(); } else if (event.key === 'ArrowRight') { this.nextImage(); } else if (event.key === 'Escape') { this.closeFullScreen(); } } }, watch: { isFullScreen(newVal) { if (newVal) { document.body.style.overflow = 'hidden'; // Prevent scrolling when full screen } else { document.body.style.overflow = ''; // Restore scrolling } } } }); // ================================================================================= // Asset Borrow/Return Widget Component // ================================================================================= Vue.component('asset-borrow-return-widget', { props: { rowData: { type: Object, required: true } }, template: `
Ausgeliehen von: {{ rowData.currentUser }}
Extern an: {{ rowData.externalUser }}
Baustelle: {{ rowData.currentSite }}
Seit: {{ formatDate(rowData.borrowDate, 'DD.MM.YYYY HH:mm') }}
Vorauss. Rückgabe: {{ formatDate(rowData.expectedReturnDate, 'DD.MM.YYYY') }}

Dauerhaft Reserviert für {{ activeReservation.userName }}
Nächste Reservierung: {{ formatDate(nextReservation.startDate, 'DD.MM.YYYY') }} bis {{ formatDate(nextReservation.endDate, 'DD.MM.YYYY') }}
({{ nextReservation.notes }})

für {{ nextReservation.userName }}
Verfügbar
Dauerhaft Reserviert für {{ activeReservation.userName }}
Nächste Reservierung: {{ formatDate(nextReservation.startDate, 'DD.MM.YYYY') }} bis {{ formatDate(nextReservation.endDate, 'DD.MM.YYYY') }}
({{ nextReservation.notes }})

für {{ nextReservation.userName }}
{{ reservationWarning }}

Gerät: {{ rowData.name }}

Mitarbeiter: {{ selectedUserName }}

Gerät: {{ rowData.name }}

Soll dieses Gerät wirklich als zurückgegeben markiert werden?

`, data() { return { window: window, userAutoCompleteUrl: window.TT_CONFIG.BASE_PATH + '/WarehouseShippingNote/userAutoComplete', selectedUserId: null, selectedUserName: '', showBorrowModal: false, showReturnModal: false, borrowSite: '', borrowReason: '', returnReason: '', externalUser: '', expectedReturnDate: null, reservationWarning: null, } }, computed: { activeReservation() { if (!this.rowData.reservations || this.rowData.reservations.length === 0) return null; const now = window.moment().unix(); return this.rowData.reservations.find(r => r.startDate <= now && (r.endDate === null || r.endDate >= now)); }, nextReservation() { if (!this.rowData.reservations || this.rowData.reservations.length === 0) return null; const now = window.moment().unix(); // Filter for reservations starting in the future const upcomingReservations = this.rowData.reservations.filter(r => r.startDate > now); if (upcomingReservations.length === 0) { return null; } // Of the upcoming reservations, find the one that starts soonest return upcomingReservations.reduce((earliest, current) => { return current.startDate < earliest.startDate ? current : earliest; }); } }, methods: { formatDate(timestamp, format) { if (!timestamp) return ''; return window.moment.unix(timestamp).format(format); }, isDatePast(timestamp) { if (!timestamp) return false; return window.moment.unix(timestamp).isBefore(window.moment(), 'day'); }, async onUserSelect(userId) { if (!userId) return; this.selectedUserId = userId; const response = await axios.get(`${this.userAutoCompleteUrl}?searchedID=${userId}`); this.selectedUserName = response.data[0]?.text || 'Unbekannt'; this.showBorrowModal = true; }, async borrowAsset(force = false) { if (!this.borrowSite) { return window.notify('error', 'Bitte Baustelle/Projekt angeben.'); } try { const payload = { assetId: this.rowData.id, userId: this.selectedUserId, site: this.borrowSite, reason: this.borrowReason, externalUser: this.externalUser, expectedReturnDate: this.expectedReturnDate, force: force }; const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/AssetManagement/borrow`, payload); if (response.data.success) { window.notify('success', response.data.message); this.showBorrowModal = false; this.$emit('update'); } else if (response.data.warning === 'conflict') { if (confirm(response.data.message)) { this.borrowAsset(true); // Retry with force flag } } else { window.notify('error', response.data.message || 'Fehler beim Ausleihen.'); } } catch (error) { window.notify('error', 'Ein Fehler ist aufgetreten.'); } }, async returnAsset() { try { const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/AssetManagement/return`, { journalId: this.rowData.journalId, reason: this.returnReason }); if (response.data.success) { window.notify('success', response.data.message); this.showReturnModal = false; this.$emit('update'); } else { window.notify('error', response.data.message || 'Fehler beim Zurückgeben.'); } } catch (error) { window.notify('error', 'Ein Fehler ist aufgetreten.'); } } } }); // ================================================================================= // Asset Create/Edit Modal // ================================================================================= Vue.component('asset-management-modal', { props: ['id'], template: `

Bildergalerie
Noch keine Bilder hochgeladen.

`, data(){ return { categoryAutoCompleteUrl: window.TT_CONFIG.BASE_PATH + '/AssetManagement/getCategories', asset: { name: '', description: '', category: '', assetNumber: '', location: 'Liftkammer', serviceDueDate: null, mustReturnDate: null, mainImageId: null, imageIds: [], }, } }, computed: { isCreateMode() { return this.id === 'create'; } }, async mounted() { if (!this.isCreateMode) { const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/AssetManagement/getById`, { params: { id: this.id } }); if (!response.data.imageIds) { response.data.imageIds = []; } else if (typeof response.data.imageIds === 'string') { response.data.imageIds = JSON.parse(response.data.imageIds); } this.asset = response.data; } else { await this.suggestAssetNumber(); } }, methods: { async submit() { const url = this.isCreateMode ? `${window.TT_CONFIG.BASE_PATH}/AssetManagement/create` : `${window.TT_CONFIG.BASE_PATH}/AssetManagement/update`; try { const response = await axios.post(url, this.asset); if (response.data.success) { window.notify('success', response.data.message || 'Erfolgreich gespeichert.'); this.$emit('close'); } else { window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.'); } } catch (error) { window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); } }, async deleteAsset() { if (!confirm('Soll dieses Gerät wirklich gelöscht werden?')) return; try { const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/AssetManagement/delete`, { id: this.id }); if (response.data.success) { window.notify('success', 'Gerät gelöscht.'); this.$emit('close'); } else { window.notify('error', response.data.message || 'Fehler beim Löschen.'); } } catch (error) { window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); } }, async suggestAssetNumber() { const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/AssetManagement/suggestAssetNumber`); if (response.data.success) { this.$set(this.asset, 'assetNumber', response.data.assetNumber); } }, setMainImage(imageId) { this.$set(this.asset, 'mainImageId', imageId); }, async deleteImage(imageId) { if (!confirm('Soll dieses Bild wirklich gelöscht werden?')) return; // If it's a new asset (not yet saved), just remove from local array if (this.isCreateMode) { this.asset.imageIds = this.asset.imageIds.filter(id => id !== imageId); if (this.asset.mainImageId === imageId) { this.asset.mainImageId = this.asset.imageIds.length > 0 ? this.asset.imageIds[0] : null; } return; } // If it's an existing asset, call the backend try { const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/AssetManagement/deleteAssetImage`, { assetId: this.id, imageId: imageId }); if (response.data.success) { window.notify('success', 'Bild gelöscht.'); // Update local asset data with the response from the server this.asset.imageIds = response.data.asset.imageIds; this.asset.mainImageId = response.data.asset.mainImageId; } else { window.notify('error', response.data.message || 'Fehler beim Löschen des Bildes.'); } } catch (error) { window.notify('error', 'Netzwerkfehler beim Löschen des Bildes.'); } }, async handleFileUpload(event) { const files = event.target.files; if (!files || files.length === 0) return; const currentImageIds = Array.isArray(this.asset.imageIds) ? [...this.asset.imageIds] : []; for (const file of files) { const formData = new FormData(); formData.append('file', file); try { const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/AssetManagement/uploadFile`, formData); if (response.data.success) { currentImageIds.push(response.data.fileId); if (!this.asset.mainImageId) { this.$set(this.asset, 'mainImageId', response.data.fileId); } window.notify('success', `Bild ${file.name} erfolgreich hochgeladen.`); } else { window.notify('error', `Upload für ${file.name} fehlgeschlagen: ${response.data.error || 'Unbekannter Fehler'}`); } } catch (error) { window.notify('error', `Fehler beim Hochladen von ${file.name}.`); } } this.$set(this.asset, 'imageIds', currentImageIds); // Clear file input to allow re-uploading the same file event.target.value = ''; } } }); // ================================================================================= // Asset Journal/History Modal // ================================================================================= Vue.component('asset-management-journal-modal', { props: { assetId: { type: Number, required: true } }, template: `
Lade...
Keine Einträge vorhanden.
`, data() { return { loading: false, journalEntries: [] } }, async mounted() { this.loading = true; try { const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/AssetManagement/getJournal`, { params: { assetId: this.assetId } }); this.journalEntries = response.data; } catch (error) { window.notify('error', 'Historie konnte nicht geladen werden.'); } finally { this.loading = false; } }, methods: { formatDate(timestamp, format) { return window.moment.unix(timestamp).format(format); } } }); // ================================================================================= // Asset Reservation Modal // ================================================================================= Vue.component('asset-reservation-modal', { props: { asset: { type: Object, required: true } }, template: `
Neue Reservierung
Lade...
Bestehende Reservierungen
Keine Reservierungen vorhanden.
`, data() { return { loading: false, reservations: [], isPermanent: false, newReservation: { userId: null, startDate: null, endDate: null, notes: '' }, userAutoCompleteUrl: window.TT_CONFIG.BASE_PATH + '/WarehouseShippingNote/userAutoComplete', } }, watch: { isPermanent(val) { if (val) { this.newReservation.endDate = null; if (!this.newReservation.startDate) { this.newReservation.startDate = window.moment().startOf('day').unix(); } } } }, async mounted() { this.fetchReservations(); }, methods: { formatDate(timestamp, format) { if (!timestamp) return ''; return window.moment.unix(timestamp).format(format); }, async fetchReservations() { this.loading = true; try { const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/AssetManagement/getReservations`, { params: { assetId: this.asset.id } }); this.reservations = response.data; } catch (error) { window.notify('error', 'Reservierungen konnten nicht geladen werden.'); } finally { this.loading = false; } }, async saveReservation() { if (!this.newReservation.userId || !this.newReservation.startDate) { return window.notify('error', 'Mitarbeiter und Startdatum sind erforderlich.'); } try { const payload = { ...this.newReservation, assetId: this.asset.id }; const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/AssetManagement/createReservation`, payload); if (response.data.success) { window.notify('success', 'Reservierung gespeichert.'); this.resetForm(); await this.fetchReservations(); } else { window.notify('error', response.data.message || 'Fehler beim Speichern.'); } } catch (error) { window.notify('error', 'Ein Fehler ist aufgetreten.'); } }, async deleteReservation(id) { if (!confirm('Soll diese Reservierung wirklich gelöscht werden?')) return; try { const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/AssetManagement/deleteReservation`, { id }); if (response.data.success) { window.notify('success', 'Reservierung gelöscht.'); await this.fetchReservations(); } else { window.notify('error', response.data.message || 'Fehler beim Löschen.'); } } catch (error) { window.notify('error', 'Ein Fehler ist aufgetreten.'); } }, resetForm() { this.newReservation = { userId: null, startDate: null, endDate: null, notes: '' }; this.isPermanent = false; } } });