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: `
{{ formatDate(row.serviceDueDate, 'DD.MM.YYYY') }}
`,
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.
-
{{ entry.userName }} @ {{ entry.site }}
{{ formatDate(entry.borrowDate, 'DD.MM.YY HH:mm') }}
Extern an: {{ entry.externalUser }}
Grund: {{ entry.borrowReason || '-' }}
Zurück am: {{ formatDate(entry.returnDate, 'DD.MM.YY HH:mm') }}
Bemerkung: {{ entry.returnReason || '-' }}
Aktuell ausgeliehen
Vorauss. Rückgabe: {{ formatDate(entry.expectedReturnDate, 'DD.MM.YYYY') }}
`,
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: `
Lade...
Bestehende Reservierungen
Keine Reservierungen vorhanden.
-
{{ res.userName }}
{{ formatDate(res.startDate, 'DD.MM.YYYY') }} - {{ res.endDate ? formatDate(res.endDate, 'DD.MM.YYYY') : 'Dauerhaft' }}
Notiz: {{ res.notes }}
`,
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;
}
}
});