769 lines
33 KiB
JavaScript
769 lines
33 KiB
JavaScript
window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [
|
|
{
|
|
"key": "status_to_progress",
|
|
"title": "In Bearbeitung",
|
|
"class": "fas fa-cog text-warning",
|
|
"condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['new', 'on_hold'].includes(row.status),
|
|
},
|
|
{
|
|
"key": "status_to_accepted",
|
|
"title": "Akzeptieren",
|
|
"class": "fas fa-check text-success",
|
|
"condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['new', 'in_progress', 'on_hold'].includes(row.status),
|
|
},
|
|
{
|
|
"key": "status_to_invoiced",
|
|
"title": "Verrechnet",
|
|
"class": "fas fa-file-invoice text-info",
|
|
"condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['in_progress', 'accepted', 'on_hold'].includes(row.status),
|
|
},
|
|
{
|
|
"key": "status_to_on_hold",
|
|
"title": "On Hold",
|
|
"class": "fas fa-pause text-warning",
|
|
"condition": (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['accepted', 'new', 'in_progress'].includes(row.status),
|
|
},
|
|
{
|
|
"key": "status_to_cancelled",
|
|
"title": "Storniert",
|
|
"class": "fas fa-ban text-danger",
|
|
"condition": (row) => (window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && ['new', 'in_progress', 'accepted'].includes(row.status)) || (row.status === 'new' && row.signature === null),
|
|
},
|
|
{
|
|
"key": "status_to_new",
|
|
"title": "Lieferschein wiedereröffnen",
|
|
"class": "fas fa-redo-alt text-success",
|
|
"condition": (row) => (window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.status === 'cancelled'),
|
|
},
|
|
{
|
|
"key": "add_log",
|
|
"title": "Log Eintrag hinzufügen",
|
|
"class": "fas fa-plus text-primary",
|
|
},
|
|
{
|
|
"key": "print",
|
|
"title": "Drucken",
|
|
"class": "fas fa-print text-primary",
|
|
}
|
|
]
|
|
|
|
// normal regie 50% 100%
|
|
|
|
window.TT_CONFIG["CRUD_CONFIG"]["editCondition"] = (row) => row.status === 'new' && row.signature === null || window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1';
|
|
|
|
Vue.component('warehouse-shipping-note-positions', {
|
|
//language=Vue
|
|
props: {
|
|
positions: Array,
|
|
hoursEntries: Array
|
|
}, data() {
|
|
return {
|
|
articleData: {}, loading: false, articlePacketData: {}, userData: {}
|
|
}
|
|
}, template: `
|
|
<div>
|
|
<div v-if="loading" class="text-center">
|
|
<i class="fa fa-spinner fa-spin"></i>
|
|
</div>
|
|
|
|
|
|
<ul v-if="!loading">
|
|
<li v-for="position in positions">
|
|
<span>{{ position.amount }}x
|
|
{{ position.article ? articleData[position.article]?.text : position.articlePacket ? articlePacketData[position.articlePacket]?.text : position.articleText ? position.articleText : position.article_text }}</span>
|
|
</li>
|
|
<template v-for="entry in hoursEntries">
|
|
<li><span>{{ entry.hourCount }}h Arbeitszeit</span></li>
|
|
<li v-if="entry.carId">{{ entry.kilometerCount }}km Anfahrt</li>
|
|
</template>
|
|
</ul>
|
|
</div>
|
|
`, async mounted() {
|
|
this.loading = true;
|
|
for (const position of this.positions) {
|
|
if (position.article) {
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseArticle/autoComplete?searchedID=' + position.article);
|
|
this.$set(this.articleData, position.article, response.data[0]);
|
|
} else if (position.articlePacket) {
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseArticlePacket/autoComplete?searchedID=' + position.articlePacket);
|
|
this.$set(this.articlePacketData, position.articlePacket, response.data[0]);
|
|
}
|
|
}
|
|
for (const entry of this.hoursEntries) {
|
|
if (entry.userId) {
|
|
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/userAutoComplete?searchedID=' + entry.userId);
|
|
this.$set(this.userData, entry.userId, response.data[0]);
|
|
}
|
|
}
|
|
this.loading = false;
|
|
}
|
|
})
|
|
|
|
Vue.component('add-log-modal-sn', {
|
|
props: {
|
|
shippingNoteId: {type: Number, required: true},
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
note: '',
|
|
file: null,
|
|
uploadedFiles: [],
|
|
submitLoading: false
|
|
};
|
|
},
|
|
methods: {
|
|
async handleFileUpload(event) {
|
|
const files = event.target.files;
|
|
if (!files.length) return;
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
try {
|
|
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/uploadFile`, formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data'
|
|
}
|
|
});
|
|
|
|
if (response.data.success) {
|
|
this.uploadedFiles.push({
|
|
id: response.data.fileId,
|
|
name: file.name
|
|
});
|
|
window.notify('success', `File "${file.name}" uploaded successfully`);
|
|
} else {
|
|
window.notify('error', `File "${file.name}" upload failed: ${response.data.error || 'Unknown error'}`);
|
|
}
|
|
} catch (error) {
|
|
window.notify('error', `Error uploading file "${file.name}"`);
|
|
}
|
|
}
|
|
|
|
// Clear the file input
|
|
event.target.value = '';
|
|
},
|
|
removeFile: index => this.uploadedFiles.splice(index, 1),
|
|
async submit() {
|
|
this.submitLoading = true;
|
|
|
|
const fileIds = this.uploadedFiles.map(file => file.id);
|
|
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/createNewLogAction`, {
|
|
shippingNoteId: this.shippingNoteId,
|
|
note: this.note,
|
|
fileIds: JSON.stringify(fileIds),
|
|
});
|
|
|
|
if (response.data.success) {
|
|
this.$emit('close');
|
|
window.notify('success', response.data.message ?? 'Log Eintrag erfolgreich erstellt');
|
|
} else {
|
|
window.notify('error',
|
|
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
this.submitLoading = false;
|
|
},
|
|
},
|
|
template: `
|
|
<tt-modal :show="true" @submit="submit" :delete="false" @update:show="$emit('close')"
|
|
title="Logeintrag hinzufügen" :save-loading="submitLoading">
|
|
<template>
|
|
|
|
<div class="form-group" style="margin: 10px 0">
|
|
<label>Dateiupload (Mehrere)</label>
|
|
<input type="file" class="form-control" @change="handleFileUpload" multiple/>
|
|
</div>
|
|
|
|
<div v-if="uploadedFiles.length" class="upload-success-alert">
|
|
<div class="alert-header">
|
|
<i class="fa fa-check-circle" aria-hidden="true"></i>
|
|
<span v-if="uploadedFiles.length === 1">Datei erfolgreich hochgeladen</span>
|
|
<span v-else>Dateien erfolgreich hochgeladen</span>
|
|
</div>
|
|
<ul class="file-list">
|
|
<li v-for="(file, index) in uploadedFiles" :key="file.id" class="file-item">
|
|
<i class="fa fa-file" aria-hidden="true"></i>
|
|
<span class="file-name">{{ file.name }}</span>
|
|
<button type="button" class="remove-btn" @click="removeFile(index)">
|
|
<i class="fa fa-times" aria-hidden="true"></i>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
|
|
<tt-textarea label="Bemerkung" v-model="note" sm/>
|
|
</template>
|
|
</tt-modal>
|
|
`
|
|
})
|
|
|
|
Vue.component('tt-file', {
|
|
props: ['id'],
|
|
data: () => ({file: null}),
|
|
async mounted() {
|
|
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/File/getById`, {params: {id: this.id}});
|
|
this.file = response.data;
|
|
},
|
|
template: `
|
|
<div>
|
|
<a :href="'/File/download?id=' + id" target="_blank" v-if="file">{{ file.filename }}</a>
|
|
<template v-else>
|
|
<div class="spinner-border spinner-border-sm text-primary" role="status"><span
|
|
class="sr-only">Loading...</span></div>
|
|
</template>
|
|
</div>
|
|
`
|
|
})
|
|
|
|
|
|
Vue.component('warehouse-shipping-note-logs', {
|
|
props: {
|
|
shippingNoteId: {type: Number, required: true},
|
|
},
|
|
data() {
|
|
return {
|
|
logs: [], loading: false,
|
|
};
|
|
},
|
|
//language=Vue
|
|
template: `
|
|
<div>
|
|
<div v-if="loading" class="text-center">
|
|
<i class="fa fa-spinner fa-spin"></i>
|
|
</div>
|
|
|
|
<div v-if="!loading">
|
|
<div v-for="log in logs">
|
|
{{ formatDate(log.create) }} ({{ getUserName(log.createBy) }}) | {{ log.message }}
|
|
<template v-if="log.fileIds">
|
|
<div v-for="file in JSON.parse(log.fileIds)">
|
|
<tt-file :id="file"/>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
async mounted() {
|
|
this.loading = true;
|
|
const response = await axios.get(window.TT_CONFIG.BASE_PATH + '/WarehouseShippingNote/getLog', {params: {shippingNoteId: this.shippingNoteId}});
|
|
this.logs = response.data;
|
|
this.loading = false;
|
|
},
|
|
methods: {
|
|
formatDate: date => window.moment(date * 1000).format('DD.MM.YYYY HH:mm'),
|
|
getUserName: id => window.TT_CONFIG.CRUD_CONFIG.columns.find(col => col.key === 'createBy')?.modal.items.find(u => u.value === id)?.text
|
|
}
|
|
})
|
|
|
|
Vue.component('warehouse-shipping-note-see-through', {
|
|
props: ['wantedState'],
|
|
//language=Vue
|
|
template: `
|
|
<div class="modal"
|
|
style="position: fixed; z-index: 9999; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center;">
|
|
<div class="modal-content"
|
|
style="background-color: #fff; border-radius: 8px; width: 95%; height: 95%; max-width: none; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column;">
|
|
<div class="modal-top-bar"
|
|
style="background: linear-gradient(135deg, #f7c423, #005384); color: white; padding: 15px 20px; border-top-left-radius: 8px; border-top-right-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
|
|
<h2 style="margin: 0; font-family: Arial, sans-serif; font-size: 20px; flex: 1;">Lieferschein Durchschau
|
|
Modus</h2>
|
|
|
|
<div style="display: flex; gap: 10px; align-items: center;">
|
|
<button @click="previousNote"
|
|
style="background: none; border: none; color: white; font-family: Arial, sans-serif; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 5px;">
|
|
<i class="fas fa-chevron-left" style="font-size: 12px;"></i> Vorheriger
|
|
</button>
|
|
<button @click="nextNote"
|
|
style="background: none; border: none; color: white; font-family: Arial, sans-serif; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 5px;">
|
|
Nächster <i class="fas fa-chevron-right" style="font-size: 12px;"></i>
|
|
</button>
|
|
</div>
|
|
<i @click="$emit('close')" class="fas fa-times"
|
|
style="font-size: 20px; cursor: pointer; margin-left: 15px;"></i>
|
|
</div>
|
|
<div class="modal-body" style="padding: 20px; display: flex; flex: 1; overflow: hidden;">
|
|
<template>
|
|
<div style="width: 50%; height: 100%; overflow: auto;">
|
|
<iframe ref="iframe" v-if="currentRow" :src="'/WarehouseShippingNote/createPDF?id=' + currentRow.id"
|
|
style="width: 100%; height: 100%; border: none;"></iframe>
|
|
</div>
|
|
<div style="width: 50%; height: 100%; padding-left: 20px; display: flex; flex-direction: column;">
|
|
<div style="margin-bottom: 20px;">
|
|
<button @click="activeTab = 'Editieren'" :style="tabStyle('Editieren')">Editieren</button>
|
|
<button @click="activeTab = 'Logs'" :style="tabStyle('Logs')">Logs</button>
|
|
|
|
<div style="margin-top: 20px;" v-if="currentRow">
|
|
<template v-for="action in window.TT_CONFIG.CRUD_CONFIG.additionalActions">
|
|
<template
|
|
v-if="typeof action.condition === 'function' ? action.condition(currentRow) : true && action.key !== 'add_log' && action.key !== 'print'">
|
|
<i @click="changeStatus(action.key, currentRow)" :class="action.class"
|
|
style="font-size: 20px; cursor: pointer; margin-right: 10px;"></i>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
|
|
</div>
|
|
<div v-if="activeTab === 'Editieren'" style="flex: 1; overflow: auto;" class="see-through-test-modal">
|
|
<warehouse-shipping-note-modal v-if="currentRow" :id="currentRow.id" @close="fetchData" ref="modal">
|
|
<template v-slot:footer-prepend v-if="wantedState === 'new'">
|
|
<button class="btn btn-warning" @click="saveAndSetToProgress">Speichern und in Bearbeitung
|
|
setzen
|
|
</button>
|
|
</template>
|
|
</warehouse-shipping-note-modal>
|
|
</div>
|
|
<div v-if="activeTab === 'Logs'" style="flex: 1; overflow: auto;" class="see-through-test-modal">
|
|
<warehouse-shipping-note-logs :shipping-note-id="currentRow.id" :key="'logs' + logModalKey"/>
|
|
<add-log-modal-sn v-if="currentRow" :shipping-note-id="currentRow.id" @close="logModalKey++"
|
|
:key="'modal'+logModalKey"/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
data() {
|
|
return {
|
|
logModalKey: 0,
|
|
currentPage: 1,
|
|
perPage: 1,
|
|
rows: [],
|
|
activeTab: 'Editieren',
|
|
window: window
|
|
}
|
|
},
|
|
computed: {
|
|
currentRow() {
|
|
return this.rows[0];
|
|
}
|
|
},
|
|
methods: {
|
|
tabStyle(tab) {
|
|
return {
|
|
padding: '10px 20px',
|
|
cursor: 'pointer',
|
|
backgroundColor: this.activeTab === tab ? '#005384' : '#f7c423',
|
|
color: 'white',
|
|
border: 'none',
|
|
borderRadius: '5px',
|
|
marginRight: '10px'
|
|
};
|
|
},
|
|
previousNote() {
|
|
if (this.currentPage > 1) {
|
|
this.currentPage--;
|
|
this.fetchData();
|
|
}
|
|
},
|
|
nextNote() {
|
|
if (this.currentPage) {
|
|
this.currentPage++;
|
|
this.fetchData();
|
|
}
|
|
},
|
|
async changeStatus(action, row) {
|
|
this.$emit(action, row);
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
await this.fetchData();
|
|
},
|
|
async fetchData() {
|
|
this.loading = true;
|
|
this.rows = [];
|
|
const response = await axios.post(window.TT_CONFIG.BASE_PATH + '/WarehouseShippingNote/get', {
|
|
pagination: {page: this.currentPage, per_page: this.perPage},
|
|
filters: {status: this.wantedState},
|
|
})
|
|
|
|
this.rows = response.data.rows;
|
|
if (this.rows.length === 0) {
|
|
this.$emit('close');
|
|
this.window.notify('info', 'Keine Lieferscheine mit diesem Status gefunden');
|
|
}
|
|
|
|
this.loading = false;
|
|
},
|
|
async saveAndSetToProgress() {
|
|
await this.$refs.modal.submit('in_progress');
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
await this.fetchData();
|
|
}
|
|
},
|
|
mounted() {
|
|
this.fetchData();
|
|
}
|
|
});
|
|
|
|
Vue.component('warehouse-article-modal', {
|
|
template: `
|
|
<tt-modal :show="true" :save="false" :delete="false" @update:show="$emit('close')"
|
|
title="Artikel Suchen">
|
|
<tt-card>
|
|
<div v-if="!isLoadingCategories" :style="{
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
|
|
gap: '10px',
|
|
padding: '10px',
|
|
marginBottom: '15px',
|
|
borderBottom: '1px solid #eee'
|
|
}">
|
|
<div v-for="category in categories"
|
|
:key="category.id"
|
|
@click="selectCategory(category)"
|
|
:style="getCategoryStyle(category)"
|
|
style="
|
|
padding: '10px 15px';
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
|
background-color: #f9f9f9;
|
|
font-size: 0.9em;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
"
|
|
:title="category.name"
|
|
@mouseover="hoverCategory($event, true)"
|
|
@mouseleave="hoverCategory($event, false)">
|
|
{{ category.name }}
|
|
</div>
|
|
</div>
|
|
<div v-else style="text-align: center; padding: 20px; color: #777;">
|
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
Lade Kategorien...
|
|
</div>
|
|
|
|
|
|
<div v-if="selectedCategory" style="padding: 0 10px 15px 10px;">
|
|
<tt-input
|
|
label="Artikel suchen"
|
|
placeholder="Titel, Artikelnummer, Beschreibung..."
|
|
v-model="searchTerm"
|
|
:sm="true"
|
|
:row="false"
|
|
type="search"
|
|
hint="Sucht in Titel, Artikelnummer und Beschreibung."
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="selectedCategory" style="padding: 0 10px 10px 10px; min-height: 150px; position: relative;">
|
|
<div v-if="isLoadingArticles" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.7); display: flex; justify-content: center; align-items: center; z-index: 10;">
|
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
<span style="margin-left: 10px;">Lade Artikel...</span>
|
|
</div>
|
|
|
|
<div v-if="!isLoadingArticles && filteredArticles.length > 0" :style="{ display: 'grid', gridTemplateColumns: '1fr', gap: '8px' }">
|
|
<div v-for="article in filteredArticles"
|
|
:key="article.id"
|
|
@click="selectArticle(article)"
|
|
style="
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 4px;
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
background-color: #fff;
|
|
transition: background-color 0.15s ease;
|
|
"
|
|
@mouseover="event => event.currentTarget.style.backgroundColor='#f5f5f5'"
|
|
@mouseleave="event => event.currentTarget.style.backgroundColor='#fff'">
|
|
<div>
|
|
<strong style="font-size: 0.95em;">{{ article.title }}</strong>
|
|
<span style="font-size: 0.85em; color: #666; margin-left: 10px;">({{ article.articleNumber }})</span>
|
|
</div>
|
|
<div v-if="article.description" style="font-size: 0.85em; color: #555; margin-top: 3px;">
|
|
{{ article.description }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!isLoadingArticles && articles.length > 0 && filteredArticles.length === 0" style="color: #777; text-align: center; padding: 20px;">
|
|
Keine Artikel entsprechen Ihrer Suche nach "{{ searchTerm }}".
|
|
</div>
|
|
|
|
<div v-if="!isLoadingArticles && articles.length === 0 && !isLoadingArticles" style="color: #777; text-align: center; padding: 20px;">
|
|
Keine Artikel in der ausgewählten Kategorie gefunden.
|
|
</div>
|
|
</div>
|
|
|
|
</tt-card>
|
|
</tt-modal>
|
|
`,
|
|
data() {
|
|
return {
|
|
window: window,
|
|
isLoadingCategories: false, // Added loading state for categories
|
|
categories: [],
|
|
selectedCategory: null,
|
|
articles: [],
|
|
searchTerm: '',
|
|
isLoadingArticles: false,
|
|
}
|
|
},
|
|
computed: {
|
|
filteredArticles() {
|
|
if (!this.searchTerm) {
|
|
return this.articles;
|
|
}
|
|
const lowerSearchTerm = this.searchTerm.toLowerCase();
|
|
// Ensure articles is an array before filtering
|
|
if (!Array.isArray(this.articles)) {
|
|
console.warn('Attempted to filter non-array articles:', this.articles);
|
|
return [];
|
|
}
|
|
return this.articles.filter(article => {
|
|
const titleMatch = article.title?.toLowerCase().includes(lowerSearchTerm);
|
|
const articleNumberMatch = article.articleNumber?.toLowerCase().includes(lowerSearchTerm);
|
|
const descriptionMatch = article.description?.toLowerCase().includes(lowerSearchTerm);
|
|
return titleMatch || articleNumberMatch || descriptionMatch;
|
|
});
|
|
}
|
|
},
|
|
methods: {
|
|
async selectCategory(category) {
|
|
if (this.selectedCategory && this.selectedCategory.id === category.id) {
|
|
return;
|
|
}
|
|
this.selectedCategory = category;
|
|
this.searchTerm = '';
|
|
this.articles = [];
|
|
console.log('Selected Category:', this.selectedCategory);
|
|
await this.fetchArticles(category.id);
|
|
},
|
|
|
|
async fetchArticles(categoryId) {
|
|
if (!categoryId) return;
|
|
this.isLoadingArticles = true;
|
|
try {
|
|
const response = await axios.post(window.TT_CONFIG.BASE_PATH + '/WarehouseArticle/getAll', {
|
|
filters: {
|
|
category_id: categoryId
|
|
}
|
|
});
|
|
// Robust check: Ensure response.data is an array
|
|
if (Array.isArray(response.data)) {
|
|
this.articles = response.data;
|
|
} else {
|
|
console.warn('Fetched articles data is not an array:', response.data);
|
|
this.articles = []; // Set to empty array if not valid
|
|
}
|
|
console.log('Fetched Articles:', this.articles);
|
|
} catch (error) {
|
|
console.error("Error fetching articles:", error);
|
|
this.articles = [];
|
|
} finally {
|
|
this.isLoadingArticles = false;
|
|
}
|
|
},
|
|
|
|
selectArticle(article) {
|
|
console.log('Selected Article:', article);
|
|
this.$emit('article-selected', article);
|
|
this.$emit('close');
|
|
},
|
|
|
|
getCategoryStyle(category) {
|
|
const style = {};
|
|
if (this.selectedCategory && this.selectedCategory.id === category.id) {
|
|
style.backgroundColor = '#d0e0ff';
|
|
style.borderColor = '#a0c0ff';
|
|
style.fontWeight = 'bold';
|
|
style.boxShadow = '0 0 5px rgba(0, 100, 255, 0.3)';
|
|
}
|
|
return style;
|
|
},
|
|
|
|
hoverCategory(event, isHovering) {
|
|
// Call findCategoryId safely
|
|
const categoryId = this.findCategoryId(event.target.innerText);
|
|
|
|
// Guard: Only proceed if categoryId was found
|
|
if (categoryId === null) {
|
|
// console.warn('Could not find category ID for:', event.target.innerText);
|
|
return; // Stop if ID couldn't be found (e.g., categories not loaded yet)
|
|
}
|
|
|
|
// Rest of the hover logic...
|
|
if (!event.target.dataset.categoryId) {
|
|
event.target.dataset.categoryId = categoryId;
|
|
}
|
|
|
|
if (!this.selectedCategory || this.selectedCategory.id != categoryId) {
|
|
if (isHovering) {
|
|
event.target.style.backgroundColor = '#e9e9e9';
|
|
event.target.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
|
|
} else {
|
|
event.target.style.backgroundColor = '#f9f9f9';
|
|
event.target.style.boxShadow = 'none';
|
|
}
|
|
} else {
|
|
if (!isHovering) {
|
|
event.target.style.boxShadow = this.getCategoryStyle(this.selectedCategory).boxShadow || 'none';
|
|
event.target.style.backgroundColor = this.getCategoryStyle(this.selectedCategory).backgroundColor ||'#d0e0ff';
|
|
}
|
|
}
|
|
},
|
|
|
|
findCategoryId(name) {
|
|
// **** GUARD ADDED HERE ****
|
|
// Check if categories is an array and has items before using .find()
|
|
if (!Array.isArray(this.categories) || this.categories.length === 0) {
|
|
// console.warn('findCategoryId called before categories array is ready or populated.');
|
|
return null; // Return null if categories are not ready
|
|
}
|
|
// Now it's safe to use .find()
|
|
const found = this.categories.find(cat => cat && cat.name === name); // Added check for cat existence
|
|
return found ? found.id : null;
|
|
}
|
|
|
|
},
|
|
async mounted() {
|
|
this.isLoadingCategories = true; // Set loading true before fetch
|
|
try {
|
|
const response = await axios.get(window.TT_CONFIG.BASE_PATH + '/WarehouseCategory/getAll');
|
|
console.log('Raw category response.data:', response.data); // Log the raw response
|
|
console.log('Is response.data an array?', Array.isArray(response.data)); // Check if it's an array
|
|
|
|
// **** ROBUST ASSIGNMENT ****
|
|
// Ensure we assign an array, even if the response isn't one.
|
|
if (Array.isArray(response.data)) {
|
|
this.categories = response.data;
|
|
} else if (response.data && Array.isArray(response.data.data)) {
|
|
// Example: Handle common case where data is nested like { data: [...] }
|
|
console.log('Assigning categories from response.data.data');
|
|
this.categories = response.data.data;
|
|
}
|
|
else {
|
|
console.warn('Categories response.data is not an array and not handled structure:', response.data);
|
|
this.categories = []; // Default to empty array if response is unexpected
|
|
}
|
|
console.log('Assigned this.categories:', this.categories);
|
|
|
|
} catch (error) {
|
|
console.error("Error fetching categories:", error);
|
|
this.categories = []; // Ensure it's an array on error
|
|
} finally {
|
|
this.isLoadingCategories = false; // Set loading false after fetch/error
|
|
}
|
|
}
|
|
})
|
|
|
|
|
|
Vue.component('warehouse-shipping-note', {
|
|
//language=Vue
|
|
template: `
|
|
<tt-card>
|
|
<warehouse-shipping-note-see-through
|
|
@close="shippingNoteSeeThrough = false;$refs.table.$refs.table.refreshTable()"
|
|
v-if="shippingNoteSeeThrough !== false"
|
|
:wanted-state="shippingNoteSeeThrough"
|
|
@status_to_progress="changeStatus($event.id, 'in_progress')"
|
|
@status_to_new="changeStatus($event.id, 'new')"
|
|
@status_to_on_hold="changeStatus($event.id, 'on_hold')"
|
|
@status_to_cancelled="changeStatus($event.id, 'cancelled')"
|
|
@status_to_invoiced="changeStatus($event.id, 'invoiced')"
|
|
@status_to_accepted="changeStatus($event.id, 'accepted')"/>
|
|
<warehouse-shipping-note-modal v-if="shippingNoteModalId" :id="shippingNoteModalId"
|
|
ref="modal"
|
|
@close="shippingNoteModalId = null;$refs.table.$refs.table.refreshTable()"
|
|
@open-article-modal="articleModalId = true;window.console.log($event)"
|
|
@open-signing-modal="signingShippingNoteId = $event"/>
|
|
|
|
<warehouse-article-modal
|
|
v-if="articleModalId"
|
|
:id="articleModalId"
|
|
@close="articleModalId = null"
|
|
@article-selected="$refs.modal.$refs.positionsManager.updateField('article', $event.id);"/>
|
|
|
|
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
|
<warehouse-shipping-note-signature-pad v-if="signingShippingNoteId" :shipping-note-id="signingShippingNoteId"
|
|
@close="signingShippingNoteId = null;shippingNoteModalId = null;$refs.table.$refs.table.refreshTable()"/>
|
|
<add-log-modal-sn v-if="addLogModalId" :shipping-note-id="addLogModalId" @close="addLogModalId = null"/>
|
|
|
|
<button @click="shippingNoteModalId = 'create'" class="btn btn-primary">Lieferschein erstellen</button>
|
|
<div class="dropdown" style="display: inline-block; margin-left: 10px;"
|
|
v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1'">
|
|
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown"
|
|
aria-haspopup="true" aria-expanded="false">
|
|
Durchschau-Modus
|
|
</button>
|
|
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" style="cursor: pointer;">
|
|
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'new'">Neue</a>
|
|
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'in_progress'">In Bearbeitung</a>
|
|
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'accepted'">Akzeptierte</a>
|
|
<a class="dropdown-item" @click="shippingNoteSeeThrough = 'on_hold'">On Hold</a>
|
|
</div>
|
|
</div>
|
|
|
|
<tt-table-crud emit-edit
|
|
@openHistory="historyModal = true; historyModalId = $event.id"
|
|
@print="window.open(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/createPDF?id=' + $event.id)"
|
|
@status_to_progress="changeStatus($event.id, 'in_progress')"
|
|
@status_to_accepted="changeStatus($event.id, 'accepted')"
|
|
@status_to_invoiced="changeStatus($event.id, 'invoiced')"
|
|
@status_to_on_hold="changeStatus($event.id, 'on_hold')"
|
|
@status_to_cancelled="changeStatus($event.id, 'cancelled')"
|
|
@status_to_new="changeStatus($event.id, 'new')"
|
|
@add_log="addLogModalId = $event.id"
|
|
@edit="shippingNoteModalId = $event.id"
|
|
ref="table">
|
|
|
|
<template v-slot:expandedRow="{ row }">
|
|
<warehouse-shipping-note-positions :positions="JSON.parse(row.positions)"
|
|
:hours-entries="JSON.parse(row.hoursEntries)"/>
|
|
<warehouse-shipping-note-logs :shipping-note-id="row.id"/>
|
|
</template>
|
|
|
|
</tt-table-crud>
|
|
</tt-card>
|
|
`, data() {
|
|
return {
|
|
window: window,
|
|
historyModal: false,
|
|
historyModalId: null,
|
|
shippingNoteModalId: null,
|
|
signingShippingNoteId: null,
|
|
addLogModalId: null,
|
|
shippingNoteSeeThrough: false,
|
|
articleModalId: null,
|
|
}
|
|
},
|
|
mounted() {
|
|
// check if get parameter doAction = createNew is set, if so open the modal with 'create' as id
|
|
if (window.location.search.includes('doAction=createNew')) {
|
|
this.shippingNoteModalId = 'create';
|
|
}
|
|
},
|
|
methods: {
|
|
async changeStatus(id, status) {
|
|
const response = await axios.post(window.TT_CONFIG.BASE_PATH + '/WarehouseShippingNote/changeStatus', {
|
|
id,
|
|
status
|
|
});
|
|
if (response.data.success) {
|
|
this.window.notify('success', response.data.message || 'Erfolgreich aktualisiert');
|
|
this.$refs.table.$refs.table.refreshTable();
|
|
} else {
|
|
this.window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
|
|
}
|
|
},
|
|
}
|
|
})
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('/WarehouseShippingNote/sw', {scope: '/'})
|
|
.then(registration => {
|
|
console.log('Patching PWA Service Worker registered with scope:', registration.scope);
|
|
})
|
|
.catch(error => {
|
|
console.error('Patching PWA Service Worker registration failed:', error);
|
|
});
|
|
}
|
|
}) |