806 lines
34 KiB
JavaScript
806 lines
34 KiB
JavaScript
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: `
|
|
<tt-card>
|
|
<asset-management-modal
|
|
v-if="modalId"
|
|
:id="modalId"
|
|
@close="modalId = null; $refs.table.$refs.table.refreshTable()"/>
|
|
<asset-management-journal-modal
|
|
v-if="journalModalAssetId"
|
|
:asset-id="journalModalAssetId"
|
|
@close="journalModalAssetId = null"/>
|
|
<asset-reservation-modal
|
|
v-if="reservationModalAsset"
|
|
:asset="reservationModalAsset"
|
|
@close="reservationModalAsset = null; $refs.table.$refs.table.refreshTable()"/>
|
|
<asset-print-label-modal
|
|
v-if="printModalAsset"
|
|
:asset="printModalAsset"
|
|
@close="printModalAsset = null"/>
|
|
|
|
<button v-if="window.TT_CONFIG.ASSET_ADMIN === '1'" @click="modalId = 'create'" class="btn btn-primary">Anlage erstellen</button>
|
|
|
|
<tt-table-crud
|
|
ref="table"
|
|
emit-edit
|
|
@edit="modalId = $event.id"
|
|
@openHistory="journalModalAssetId = $event.id"
|
|
@reserve="reservationModalAsset = $event"
|
|
@print="printModalAsset = $event"
|
|
:crud-config="crudConfig">
|
|
|
|
<template v-slot:assetdetails="{ row }">
|
|
<div class="d-flex align-items-center">
|
|
<tt-asset-image :image-id="row.mainImageId" :all-image-ids="row.imageIds" class="mr-3"/>
|
|
<div>
|
|
<strong v-html="row.name.replace(/\\n/g, '<br>')"></strong><br>
|
|
<small class="text-muted">{{ row.assetNumber }}</small><br>
|
|
<small v-if="row.description" v-html="row.description.replace(/\\n/g, '<br>')"></small>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-slot:currentuser="{ row }">
|
|
<asset-borrow-return-widget
|
|
:row-data="row"
|
|
@update="updateTableWithoutModalsOpening"/>
|
|
</template>
|
|
|
|
<template v-slot:serviceduedate="{ row }">
|
|
<span v-if="row.serviceDueDate" :class="{'text-danger font-weight-bold': isDatePast(row.serviceDueDate)}">
|
|
{{ formatDate(row.serviceDueDate, 'DD.MM.YYYY') }}
|
|
</span>
|
|
</template>
|
|
|
|
</tt-table-crud>
|
|
</tt-card>
|
|
`,
|
|
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: `
|
|
<tt-modal :show="true" :title="'Label für ' + asset.name + ' drucken'" :save="false" :delete="false" @update:show="$emit('close')">
|
|
<div class="text-center">
|
|
<p>Wählen Sie die gewünschte Label-Größe:</p>
|
|
<tt-button text="25mm" @click="printLabel(25)" additional-class="btn-primary mr-2"/>
|
|
<tt-button text="50mm" @click="printLabel(50)" additional-class="btn-primary"/>
|
|
</div>
|
|
</tt-modal>
|
|
`,
|
|
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: `
|
|
<div class="asset-image-container" :style="{ cursor: imageId ? 'pointer' : 'default' }">
|
|
<img v-if="imageId" :src="'/File/show?id=' + imageId + '&size=small'" @error="onImageError" @click="openFullScreen(imageId)" class="asset-image"/>
|
|
<div v-else class="asset-image-placeholder">
|
|
<i class="fas fa-camera"></i>
|
|
</div>
|
|
|
|
<div v-if="isFullScreen"
|
|
class="tt-fullscreen-image-overlay"
|
|
@click.self="closeFullScreen"
|
|
@keydown.esc="closeFullScreen"
|
|
tabindex="-1"
|
|
ref="fullScreenOverlay">
|
|
<img :src="'/File/show?id=' + currentFullScreenImageId"
|
|
class="tt-fullscreen-image"
|
|
@click.stop />
|
|
<button class="tt-fullscreen-close-btn" @click="closeFullScreen">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
|
|
<div v-if="allImageIds.length > 1" class="tt-fullscreen-gallery-nav">
|
|
<button class="nav-arrow left" @click.stop="prevImage">
|
|
<i class="fas fa-chevron-left"></i>
|
|
</button>
|
|
<div class="gallery-thumbnails">
|
|
<img v-for="imgId in allImageIds"
|
|
:key="imgId"
|
|
:src="'/File/show?id=' + imgId + '&size=tiny'"
|
|
:class="{'active': imgId === currentFullScreenImageId}"
|
|
@click.stop="setFullScreenImage(imgId)"
|
|
class="thumbnail" />
|
|
</div>
|
|
<button class="nav-arrow right" @click.stop="nextImage">
|
|
<i class="fas fa-chevron-right"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
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: `
|
|
<div>
|
|
<div v-if="rowData.currentUserId">
|
|
<div><strong>Ausgeliehen von:</strong> {{ rowData.currentUser }}</div>
|
|
<div v-if="rowData.externalUser" class="text-info"><strong>Extern an:</strong> {{ rowData.externalUser }}</div>
|
|
<div><strong>Baustelle:</strong> {{ rowData.currentSite }}</div>
|
|
<div><strong>Seit:</strong> {{ formatDate(rowData.borrowDate, 'DD.MM.YYYY HH:mm') }}</div>
|
|
<div v-if="rowData.expectedReturnDate">
|
|
<strong>Vorauss. Rückgabe:</strong>
|
|
<span :class="{'text-danger': isDatePast(rowData.expectedReturnDate)}">
|
|
{{ formatDate(rowData.expectedReturnDate, 'DD.MM.YYYY') }}
|
|
</span>
|
|
</div>
|
|
<tt-button
|
|
v-if="window.TT_CONFIG.ASSET_ADMIN === '1'"
|
|
sm
|
|
text="Zurückgeben"
|
|
@click="showReturnModal = true"
|
|
additional-class="btn-success mt-2"/>
|
|
<hr>
|
|
<div v-if="activeReservation" class="text-danger small">
|
|
<i class="fas fa-clock"></i> Dauerhaft Reserviert für {{ activeReservation.userName }}
|
|
</div>
|
|
<div v-if="nextReservation" class="text-warning small">
|
|
<i class="fas fa-calendar-alt"></i> Nächste Reservierung: {{ formatDate(nextReservation.startDate, 'DD.MM.YYYY') }}
|
|
<span v-if="nextReservation.endDate"> bis {{ formatDate(nextReservation.endDate, 'DD.MM.YYYY') }}</span>
|
|
<span v-if="nextReservation.notes"><br> ({{ nextReservation.notes }})</span>
|
|
<span v-if="nextReservation.userName"><br> für {{ nextReservation.userName }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<div class="text-success">Verfügbar</div>
|
|
<div v-if="activeReservation" class="text-danger small">
|
|
<i class="fas fa-clock"></i> Dauerhaft Reserviert für {{ activeReservation.userName }}
|
|
</div>
|
|
<div v-if="nextReservation" class="text-warning small">
|
|
<i class="fas fa-calendar-alt"></i> Nächste Reservierung: {{ formatDate(nextReservation.startDate, 'DD.MM.YYYY') }}
|
|
<span v-if="nextReservation.endDate"> bis {{ formatDate(nextReservation.endDate, 'DD.MM.YYYY') }}</span>
|
|
<span v-if="nextReservation.notes"><br> ({{ nextReservation.notes }})</span>
|
|
<span v-if="nextReservation.userName"><br> für {{ nextReservation.userName }}</span>
|
|
</div>
|
|
<tt-autocomplete
|
|
v-if="window.TT_CONFIG.ASSET_ADMIN === '1'"
|
|
:label="null"
|
|
:api-url="userAutoCompleteUrl"
|
|
placeholder="Mitarbeiter für Ausleihe..."
|
|
sm
|
|
no-form-group
|
|
v-model="selectedUserId"
|
|
@input="onUserSelect"
|
|
/>
|
|
</div>
|
|
|
|
<tt-modal v-if="showBorrowModal" :show.sync="showBorrowModal" title="Gerät ausleihen" @submit="borrowAsset()">
|
|
<div v-if="reservationWarning" class="alert alert-warning">{{ reservationWarning }}</div>
|
|
<p><strong>Gerät:</strong> {{ rowData.name }}</p>
|
|
<p><strong>Mitarbeiter:</strong> {{ selectedUserName }}</p>
|
|
<tt-input label="Baustelle / Projekt" v-model="borrowSite" sm row required/>
|
|
<tt-input label="Externer Entleiher (optional)" v-model="externalUser" sm row/>
|
|
<tt-date-picker label="Vorauss. Rückgabedatum" v-model="expectedReturnDate" :date-range="false" sm row/>
|
|
<tt-textarea label="Grund (optional)" v-model="borrowReason" sm row/>
|
|
</tt-modal>
|
|
|
|
<tt-modal v-if="showReturnModal" :show.sync="showReturnModal" title="Gerät zurückgeben" @submit="returnAsset">
|
|
<p><strong>Gerät:</strong> {{ rowData.name }}</p>
|
|
<p>Soll dieses Gerät wirklich als zurückgegeben markiert werden?</p>
|
|
<tt-textarea label="Bemerkung (optional)" v-model="returnReason" sm row/>
|
|
</tt-modal>
|
|
</div>
|
|
`,
|
|
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: `
|
|
<tt-modal
|
|
:show="true"
|
|
:title="isCreateMode ? 'Gerät anlegen' : 'Gerät bearbeiten'"
|
|
@update:show="$emit('close')"
|
|
@submit="submit"
|
|
:delete="!isCreateMode"
|
|
@delete="deleteAsset"
|
|
>
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<label>Hauptbild</label>
|
|
<tt-asset-image :image-id="asset.mainImageId" class="mb-2 asset-main-image"/>
|
|
|
|
<label for="asset-images-upload">Bilder hochladen (mehrere möglich)</label>
|
|
<input type="file" @change="handleFileUpload" multiple class="form-control-file" id="asset-images-upload" accept="image/*"/>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<tt-input label="Gerätename" v-model="asset.name" sm required/>
|
|
<tt-input label="Kennzeichen / Nr." v-model="asset.assetNumber" sm required/>
|
|
<tt-input label="Lagerort" v-model="asset.location" sm required/>
|
|
<tt-autocomplete
|
|
label="Kategorie"
|
|
:api-url="categoryAutoCompleteUrl"
|
|
v-model="asset.category"
|
|
:return-text="true"
|
|
placeholder="Kategorie eingeben..."
|
|
sm
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<hr/>
|
|
<h5>Bildergalerie</h5>
|
|
<div v-if="!asset.imageIds || asset.imageIds.length === 0" class="text-muted text-center p-3">
|
|
Noch keine Bilder hochgeladen.
|
|
</div>
|
|
<div v-else class="asset-gallery-upload-grid">
|
|
<div v-for="imgId in asset.imageIds" :key="imgId"
|
|
class="gallery-item"
|
|
:class="{ 'is-main': imgId === asset.mainImageId }">
|
|
|
|
<tt-asset-image :image-id="imgId" />
|
|
|
|
<div class="gallery-item-actions">
|
|
<button @click="setMainImage(imgId)" title="Als Hauptbild festlegen" class="btn btn-sm btn-outline-success">
|
|
<i class="fas fa-star"></i>
|
|
</button>
|
|
<button @click="deleteImage(imgId)" title="Bild löschen" class="btn btn-sm btn-outline-danger">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr/>
|
|
<tt-date-picker label="Nächstes Service" v-model="asset.serviceDueDate" :date-range="false" sm row/>
|
|
<tt-date-picker label="Muss zurück am (für Mietgeräte)" v-model="asset.mustReturnDate" :date-range="false" sm row/>
|
|
<tt-textarea label="Beschreibung" v-model="asset.description" sm row/>
|
|
</tt-modal>
|
|
`,
|
|
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: `
|
|
<tt-modal :show="true" title="Gerätehistorie" :save="false" :delete="false" @update:show="$emit('close')">
|
|
<div v-if="loading" class="text-center"><i class="fas fa-spinner fa-spin"></i> Lade...</div>
|
|
<div v-else>
|
|
<div v-if="!journalEntries.length" class="text-center text-muted">Keine Einträge vorhanden.</div>
|
|
<ul v-else class="list-group">
|
|
<li v-for="entry in journalEntries" class="list-group-item">
|
|
<div class="d-flex w-100 justify-content-between">
|
|
<h5 class="mb-1">{{ entry.userName }} @ {{ entry.site }}</h5>
|
|
<small>{{ formatDate(entry.borrowDate, 'DD.MM.YY HH:mm') }}</small>
|
|
</div>
|
|
<p v-if="entry.externalUser" class="mb-1"><strong>Extern an:</strong> <span class="text-info">{{ entry.externalUser }}</span></p>
|
|
<p class="mb-1"><strong>Grund:</strong> {{ entry.borrowReason || '-' }}</p>
|
|
<div v-if="entry.returnDate">
|
|
<small class="text-success">
|
|
<strong>Zurück am:</strong> {{ formatDate(entry.returnDate, 'DD.MM.YY HH:mm') }}
|
|
<br>
|
|
<strong>Bemerkung:</strong> {{ entry.returnReason || '-' }}
|
|
</small>
|
|
</div>
|
|
<div v-else>
|
|
<small class="text-warning"><strong>Aktuell ausgeliehen</strong></small><br>
|
|
<small v-if="entry.expectedReturnDate"><strong>Vorauss. Rückgabe:</strong> {{ formatDate(entry.expectedReturnDate, 'DD.MM.YYYY') }}</small>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</tt-modal>
|
|
`,
|
|
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: `
|
|
<tt-modal :show="true" :title="'Reservierungen für ' + asset.name" :save="false" :delete="false" @update:show="$emit('close')">
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Neue Reservierung</h5>
|
|
<tt-autocomplete label="Mitarbeiter" :api-url="userAutoCompleteUrl" v-model="newReservation.userId" sm row/>
|
|
<tt-date-picker label="Startdatum" v-model="newReservation.startDate" :date-range="false" :time-picker="false" sm row/>
|
|
<tt-date-picker label="Enddatum" v-model="newReservation.endDate" :date-range="false" :time-picker="false" :disabled="isPermanent === 1" sm row/>
|
|
<tt-checkbox label="Dauerhaft" v-model="isPermanent" sm row/>
|
|
<tt-textarea label="Notizen" v-model="newReservation.notes" sm row/>
|
|
<tt-button text="Reservierung speichern" @click="saveReservation" additional-class="btn-primary float-right"/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading" class="text-center"><i class="fas fa-spinner fa-spin"></i> Lade...</div>
|
|
<div v-else>
|
|
<h5>Bestehende Reservierungen</h5>
|
|
<div v-if="!reservations.length" class="text-center text-muted">Keine Reservierungen vorhanden.</div>
|
|
<ul v-else class="list-group">
|
|
<li v-for="res in reservations" class="list-group-item d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong>{{ res.userName }}</strong><br>
|
|
<small>
|
|
{{ formatDate(res.startDate, 'DD.MM.YYYY') }} - {{ res.endDate ? formatDate(res.endDate, 'DD.MM.YYYY') : 'Dauerhaft' }}
|
|
</small>
|
|
<div v-if="res.notes" class="small text-muted">Notiz: {{ res.notes }}</div>
|
|
</div>
|
|
<tt-button icon="fas fa-trash" @click="deleteReservation(res.id)" additional-class="btn-danger" sm/>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</tt-modal>
|
|
`,
|
|
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;
|
|
}
|
|
}
|
|
});
|