Xinon mobile/improve

This commit is contained in:
Luca Haid
2026-01-17 12:48:08 +00:00
parent 51c9c5ae7e
commit 1426d769d2
37 changed files with 7502 additions and 1042 deletions
@@ -180,3 +180,411 @@ input:checked + .ios-switch-slider:before {
input:disabled + .ios-switch-slider {
cursor: not-allowed;
}
/* ===== ORDER DETAIL REDESIGN ===== */
.order-detail-container {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.order-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.order-number {
font-size: 1.25rem;
font-weight: 700;
color: #1a1a2e;
}
.order-meta {
font-size: 0.875rem;
color: #6c757d;
}
.order-meta span {
margin-right: 12px;
}
.order-actions {
display: flex;
align-items: center;
gap: 12px;
}
.status-badge {
padding: 6px 14px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-new { background: #e3f2fd; color: #1565c0; }
.status-accepted { background: #e8f5e9; color: #2e7d32; }
.status-ordered { background: #fff3e0; color: #ef6c00; }
.status-sent { background: #fce4ec; color: #c2185b; }
.status-partiallyDelivered { background: #fff8e1; color: #f9a825; }
.status-fullyDelivered { background: #e8f5e9; color: #2e7d32; }
.status-cancelled { background: #ffebee; color: #c62828; }
/* Status Form */
.status-form-container {
background: #fff;
border: 2px solid #e3f2fd;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.status-form-header {
display: flex;
gap: 16px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.status-form-header > div {
flex: 1;
min-width: 200px;
}
.delivery-table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
.delivery-table th {
text-align: left;
padding: 10px 12px;
background: #f8f9fa;
font-weight: 600;
font-size: 0.8rem;
text-transform: uppercase;
color: #6c757d;
border-bottom: 2px solid #dee2e6;
}
.delivery-table td {
padding: 12px;
border-bottom: 1px solid #e9ecef;
}
.delivery-table input[type="number"] {
width: 80px;
padding: 6px 10px;
border: 1px solid #ced4da;
border-radius: 6px;
text-align: center;
}
.delivery-table input[type="text"] {
width: 100%;
padding: 6px 10px;
border: 1px solid #ced4da;
border-radius: 6px;
}
.status-form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e9ecef;
}
.file-upload-row {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-top: 16px;
}
.file-upload-item {
flex: 1;
min-width: 200px;
}
/* Section Titles */
.section-title {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #6c757d;
margin: 24px 0 12px 0;
display: flex;
align-items: center;
gap: 8px;
}
.section-title i {
color: #1976d2;
}
/* Positions Table */
.positions-container {
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border: 1px solid #e0e0e0;
margin-bottom: 24px;
background: #fff;
}
.positions-table {
display: table !important;
width: 100%;
border-collapse: collapse;
background: #fff;
}
.positions-table thead {
display: table-header-group !important;
}
.positions-table tbody {
display: table-row-group !important;
}
.positions-table tfoot {
display: table-footer-group !important;
}
.positions-table tr {
display: table-row !important;
}
.positions-table th,
.positions-table td {
display: table-cell !important;
vertical-align: middle;
position: static !important;
}
.positions-table th {
text-align: left;
padding: 14px 20px;
background: #f8f9fa;
font-weight: 600;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.3px;
color: #495057;
border-bottom: 2px solid #dee2e6;
position: static !important;
}
/* Right-align numeric columns (Menge, Einzelpreis, Summe) */
.positions-table th:nth-child(2),
.positions-table th:nth-child(3),
.positions-table th:nth-child(4),
.positions-table td:nth-child(2),
.positions-table td:nth-child(3),
.positions-table td:nth-child(4) {
text-align: right;
}
.positions-table td {
padding: 12px 20px;
border-bottom: 1px solid #e9ecef;
font-size: 0.95rem;
color: #333;
}
.positions-table tbody td:first-child {
font-weight: 500;
color: #1a1a2e;
}
.positions-table tbody tr:nth-child(even) {
background: #fafbfc;
}
.positions-table tbody tr:hover {
background: #f0f4f8;
}
/* Total row in tfoot */
.positions-table tfoot .total-row {
background: #f0f0f0;
}
.positions-table tfoot .total-row td {
color: #333;
font-weight: 600;
font-size: 1rem;
padding: 14px 20px;
border-bottom: none;
border-top: 2px solid #dee2e6;
}
.positions-table tfoot .total-row td:first-child {
font-weight: 700;
}
/* Movements Table */
.movements-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.movements-table th {
text-align: left;
padding: 8px 12px;
background: #f8f9fa;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
color: #6c757d;
}
.movements-table td {
padding: 10px 12px;
border-bottom: 1px solid #e9ecef;
}
.movements-table a {
color: #1976d2;
text-decoration: none;
font-weight: 500;
}
.movements-table a:hover {
text-decoration: underline;
}
.movement-qty {
color: #2e7d32;
font-weight: 600;
}
/* Timeline */
.timeline-container {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.timeline {
position: relative;
padding-left: 24px;
margin-left: 8px;
}
.timeline::before {
content: '';
position: absolute;
left: 3px;
top: 6px;
bottom: 6px;
width: 2px;
background: #dee2e6;
border-radius: 1px;
}
.timeline-item {
position: relative;
padding-bottom: 20px;
}
.timeline-item:last-child {
padding-bottom: 0;
}
.timeline-marker {
position: absolute;
left: -21px;
top: 4px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #fff;
border: 2px solid #1976d2;
z-index: 1;
}
.timeline-item.is-first .timeline-marker {
background: #1976d2;
width: 12px;
height: 12px;
left: -22px;
top: 3px;
}
.timeline-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.timeline-date {
font-size: 0.8rem;
color: #6c757d;
font-weight: 500;
}
.timeline-author {
font-size: 0.8rem;
color: #1976d2;
font-weight: 600;
}
.timeline-body {
font-size: 0.9rem;
color: #333;
line-height: 1.5;
white-space: pre-line;
}
.timeline-files {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.timeline-files a {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: #e3f2fd;
color: #1565c0;
border-radius: 4px;
font-size: 0.8rem;
text-decoration: none;
}
.timeline-files a:hover {
background: #bbdefb;
}
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
padding: 40px;
}
+452 -56
View File
@@ -11,20 +11,32 @@ Vue.component('change-status-modal', {
note: '',
file: null,
uploadedFiles: [],
deliveryNoteFiles: [],
sendEmail: false,
sendEmailViewedPDF: false,
sendEmailMail: '',
submitLoading: false,
deliveredPositions: {} // To track delivery details for each position
deliveredPositions: {}, // To track delivery details for each position
warehouseLocations: [],
selectedLocationId: null
};
},
async mounted() {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.orderId}});
if (response.data.status === 'cancelled') {
const [orderResponse, locationsResponse] = await Promise.all([
axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.orderId}}),
axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getLocations`)
]);
if (orderResponse.data.status === 'cancelled') {
this.$emit('close');
window.notify('error', 'Bestellung wurde storniert');
}
this.order = response.data;
this.order = orderResponse.data;
this.warehouseLocations = locationsResponse.data;
// Set default location to "K1 Fladnitz 150" if available
const defaultLocation = this.warehouseLocations.find(loc => loc.text === 'K1 Fladnitz 150');
this.selectedLocationId = defaultLocation ? defaultLocation.value : (this.warehouseLocations[0]?.value || null);
// Initialize deliveredPositions after fetching the order
if (this.order && this.order.positions) {
@@ -40,6 +52,10 @@ Vue.component('change-status-modal', {
}
},
computed: {
movementPreviewCount() {
if (!this.deliveredPositions) return 0;
return Object.values(this.deliveredPositions).filter(p => p.amount > 0).length;
},
availableStatuses() {
// This computed property remains unchanged
switch (this.order.status) {
@@ -86,8 +102,7 @@ Vue.component('change-status-modal', {
}
},
methods: {
async handleFileUpload(event) {
// This method remains unchanged
async handleFileUpload(event, isDeliveryNote = false) {
const files = event.target.files;
if (!files.length) return;
@@ -104,16 +119,21 @@ Vue.component('change-status-modal', {
});
if (response.data.success) {
this.uploadedFiles.push({
const fileEntry = {
id: response.data.fileId,
name: file.name
});
window.notify('success', `File "${file.name}" uploaded successfully`);
};
if (isDeliveryNote) {
this.deliveryNoteFiles.push(fileEntry);
} else {
this.uploadedFiles.push(fileEntry);
}
window.notify('success', `Datei "${file.name}" erfolgreich hochgeladen`);
} else {
window.notify('error', `File "${file.name}" upload failed: ${response.data.error || 'Unknown error'}`);
window.notify('error', `Datei "${file.name}" Upload fehlgeschlagen: ${response.data.error || 'Unbekannter Fehler'}`);
}
} catch (error) {
window.notify('error', `Error uploading file "${file.name}"`);
window.notify('error', `Fehler beim Hochladen von "${file.name}"`);
}
}
event.target.value = '';
@@ -121,6 +141,9 @@ Vue.component('change-status-modal', {
removeFile(index) {
this.uploadedFiles.splice(index, 1)
},
removeDeliveryNoteFile(index) {
this.deliveryNoteFiles.splice(index, 1)
},
async submit() {
this.submitLoading = true;
if (this.newStatus === 'accepted' && this.sendEmail && !this.sendEmailMail) {
@@ -139,6 +162,7 @@ Vue.component('change-status-modal', {
}
const fileIds = this.uploadedFiles.map(file => file.id);
const deliveryNoteFileIds = this.deliveryNoteFiles.map(file => file.id);
// Prepare delivery data if the status is related to delivery
let deliveryData = null;
@@ -151,7 +175,9 @@ Vue.component('change-status-modal', {
status: this.newStatus,
note: this.note,
fileIds: JSON.stringify(fileIds),
deliveryData: deliveryData // Send the new delivery data to the backend
deliveryData: deliveryData, // Send the new delivery data to the backend
locationId: this.selectedLocationId,
deliveryNoteFileIds: deliveryNoteFileIds
});
if (response.data.success) {
@@ -230,6 +256,12 @@ Vue.component('change-status-modal', {
<tt-textarea label="Bemerkung" v-model="note" sm/>
<div v-if="newStatus === 'partiallyDelivered' || newStatus === 'fullyDelivered'">
<h4 class="mt-3">Lagerstandort</h4>
<tt-select label="Lagerstandort für Einbuchung"
v-model="selectedLocationId"
:options="warehouseLocations"
sm row/>
<h4 class="mt-3">Positionen Lieferung erfassen</h4>
<div style="display: grid; grid-template-columns: 2fr 1fr 1fr 2fr 1fr; grid-gap: 10px; font-weight: bold; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid #ccc;">
<div>Artikel</div>
@@ -270,6 +302,32 @@ Vue.component('change-status-modal', {
</div>
</div>
</template>
<div class="alert alert-info mt-3" v-if="movementPreviewCount > 0">
<i class="fas fa-info-circle"></i>
Es werden <strong>{{ movementPreviewCount }}</strong> Lagerbewegung(en) erstellt.
</div>
<h4 class="mt-3">Lieferschein Foto</h4>
<div class="form-group">
<label>Lieferschein hochladen</label>
<input type="file" class="form-control" @change="handleFileUpload($event, true)" multiple accept="image/*,.pdf"/>
</div>
<div v-if="deliveryNoteFiles.length" class="upload-success-alert mb-3">
<div class="alert-header">
<i class="fa fa-check-circle" aria-hidden="true"></i>
<span>Lieferschein hochgeladen</span>
</div>
<ul class="file-list">
<li v-for="(file, index) in deliveryNoteFiles" :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="removeDeliveryNoteFile(index)">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</li>
</ul>
</div>
</div>
<div v-if="newStatus === 'accepted'">
@@ -581,60 +639,399 @@ Vue.component('tt-file', {
Vue.component('warehouse-order-detail', {
template: `
<tt-card>
<template v-slot:header><h4>Bestellungsdetails für #{{ loading ? 'Laden...' : order.orderNumber }}</h4></template>
<div class="order-detail-container">
<template v-if="loading">
<div class="d-flex justify-content-center align-items-center">
<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="sr-only">Loading...</span></div>
<div class="loading-spinner">
<div class="spinner-border text-primary" role="status"><span class="sr-only">Laden...</span></div>
</div>
</template>
<template v-else>
<h3>Positionen</h3>
<div class="grid-container header">
<div v-for="header in ['Artikel', 'Menge', 'Preis', 'Lieferant', 'Verwendung', 'Summe']"><strong>{{ header }}</strong></div>
</div>
<div class="grid-container" v-for="p in order.positions">
<div>{{ p.articleName }}</div>
<div>{{ p.amount }}</div>
<div>{{ p.buyPrice }}</div>
<div>{{ p.distributorName }}</div>
<div>{{ p.verwendung }}</div>
<div>{{ p.amount * p.buyPrice }}</div>
</div>
<template v-if="orderLog?.length > 0">
<hr>
<h3>Log</h3>
<div v-for="log in orderLog">
{{ 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>
<template v-else>
<!-- HEADER -->
<div class="order-header">
<div class="order-info">
<span class="order-number">#{{ order.orderNumber }}</span>
<div class="order-meta">
<span><i class="fas fa-truck"></i> {{ order.distributorName || 'Kein Lieferant' }}</span>
<span><i class="fas fa-box"></i> {{ order.positions?.length || 0 }} Positionen</span>
<span><i class="fas fa-euro-sign"></i> {{ formatCurrency(grandTotal) }}</span>
</div>
</div>
<div class="order-actions">
<span :class="['status-badge', 'status-' + order.status]">{{ statusLabel }}</span>
<button v-if="!showStatusForm && order.status !== 'cancelled' && order.status !== 'fullyDelivered'"
@click="toggleStatusForm"
class="btn btn-primary btn-sm">
<i class="fas fa-edit"></i> Status ändern
</button>
</div>
</div>
<!-- INLINE STATUS FORM -->
<div v-if="showStatusForm" class="status-form-container">
<div class="status-form-header">
<div>
<label class="form-label"><strong>Neuer Status</strong></label>
<select v-model="newStatus" class="form-control form-control-sm">
<option v-for="s in availableStatuses" :value="s.value">{{ s.text }}</option>
</select>
</div>
<div v-if="isDeliveryStatus">
<label class="form-label"><strong>Lagerstandort</strong></label>
<select v-model="selectedLocationId" class="form-control form-control-sm">
<option v-for="loc in warehouseLocations" :value="loc.value">{{ loc.text }}</option>
</select>
</div>
</div>
<template v-if="isDeliveryStatus">
<table class="delivery-table">
<thead>
<tr>
<th>Artikel</th>
<th style="width: 80px;">Bestellt</th>
<th style="width: 100px;">Geliefert</th>
<th>Grund für Abweichung</th>
<th style="width: 80px; text-align: center;">Rest stornieren</th>
</tr>
</thead>
<tbody>
<tr v-for="(pos, index) in order.positions" :key="index">
<td>{{ pos.articleName }}</td>
<td class="text-center">{{ pos.amount }}</td>
<td>
<input type="number" v-model.number="deliveredPositions[index].amount" :max="pos.amount" min="0"/>
</td>
<td>
<input type="text"
v-if="deliveredPositions[index].amount < pos.amount"
v-model="deliveredPositions[index].reason"
placeholder="z.B. Lieferschaden"/>
</td>
<td class="text-center">
<input type="checkbox"
v-if="deliveredPositions[index].amount < pos.amount"
v-model="deliveredPositions[index].cancelRest"/>
</td>
</tr>
</tbody>
</table>
<div v-if="movementPreviewCount > 0" class="alert alert-info" style="margin: 12px 0;">
<i class="fas fa-info-circle"></i>
Es werden <strong>{{ movementPreviewCount }}</strong> Lagerbewegung(en) erstellt.
</div>
</template>
<div class="file-upload-row">
<div class="file-upload-item">
<label class="form-label"><i class="fas fa-file"></i> Datei anhängen</label>
<input type="file" class="form-control form-control-sm" @change="handleFileUpload($event, false)" multiple/>
<div v-if="uploadedFiles.length" class="mt-2">
<span v-for="(f, i) in uploadedFiles" :key="f.id" class="badge bg-success me-1">
{{ f.name }} <i class="fas fa-times" style="cursor:pointer" @click="uploadedFiles.splice(i, 1)"></i>
</span>
</div>
</div>
<div v-if="isDeliveryStatus" class="file-upload-item">
<label class="form-label"><i class="fas fa-camera"></i> Lieferschein Foto</label>
<input type="file" class="form-control form-control-sm" @change="handleFileUpload($event, true)" accept="image/*,.pdf"/>
<div v-if="deliveryNoteFiles.length" class="mt-2">
<span v-for="(f, i) in deliveryNoteFiles" :key="f.id" class="badge bg-primary me-1">
{{ f.name }} <i class="fas fa-times" style="cursor:pointer" @click="deliveryNoteFiles.splice(i, 1)"></i>
</span>
</div>
</div>
</div>
<div style="margin-top: 12px;">
<label class="form-label"><i class="fas fa-comment"></i> Bemerkung</label>
<textarea v-model="note" class="form-control form-control-sm" rows="2" placeholder="Optionale Bemerkung..."></textarea>
</div>
<div class="status-form-actions">
<button @click="cancelStatusChange" class="btn btn-outline-secondary btn-sm">Abbrechen</button>
<button @click="submitStatusChange" :disabled="isSubmitting" class="btn btn-success btn-sm">
<i class="fas fa-check"></i> {{ isSubmitting ? 'Wird gespeichert...' : 'Speichern' }}
</button>
</div>
</div>
<!-- POSITIONEN -->
<div class="section-title"><i class="fas fa-list"></i> Positionen</div>
<div class="positions-container">
<table class="positions-table">
<thead>
<tr>
<th>Artikel</th>
<th>Menge</th>
<th>Einzelpreis</th>
<th>Summe</th>
</tr>
</thead>
<tbody>
<tr v-for="p in order.positions" :key="p.article">
<td>{{ p.articleName }}</td>
<td>{{ p.amount }} Stk</td>
<td>{{ formatCurrency(p.buyPrice) }}</td>
<td>{{ formatCurrency(p.amount * p.buyPrice) }}</td>
</tr>
</tbody>
<tfoot>
<tr class="total-row">
<td>Gesamtsumme</td>
<td></td>
<td></td>
<td>{{ formatCurrency(grandTotal) }}</td>
</tr>
</tfoot>
</table>
</div>
<!-- LAGERBEWEGUNGEN -->
<template v-if="linkedMovements?.length > 0">
<div class="section-title"><i class="fas fa-warehouse"></i> Lagerbewegungen</div>
<table class="movements-table">
<thead>
<tr>
<th>Nummer</th>
<th>Artikel</th>
<th>Menge</th>
<th>Lagerort</th>
<th>Datum</th>
</tr>
</thead>
<tbody>
<tr v-for="m in linkedMovements" :key="m.id">
<td><a :href="window.TT_CONFIG.BASE_PATH + '/WarehouseMovement?showId=' + m.id" target="_blank">{{ m.movementNumber }}</a></td>
<td>{{ m.articleName }}</td>
<td class="movement-qty">+{{ m.quantity }}</td>
<td>{{ m.locationName }}</td>
<td>{{ formatDate(m.create) }}</td>
</tr>
</tbody>
</table>
</template>
<!-- AKTIVITÄT / TIMELINE -->
<template v-if="sortedLog?.length > 0">
<div class="section-title"><i class="fas fa-history"></i> Aktivität</div>
<div class="timeline-container">
<div class="timeline">
<div v-for="(log, index) in sortedLog" :key="log.id || index" class="timeline-item" :class="{ 'is-first': index === 0 }">
<div class="timeline-marker"></div>
<div class="timeline-content">
<div class="timeline-header">
<span class="timeline-date">{{ formatDate(log.create) }}</span>
<span class="timeline-author">{{ getUserName(log.createBy) }}</span>
</div>
<div class="timeline-body">{{ log.message }}</div>
<div v-if="log.fileIds && JSON.parse(log.fileIds).length > 0" class="timeline-files">
<a v-for="fileId in JSON.parse(log.fileIds)" :key="fileId" :href="'/File/download?id=' + fileId" target="_blank">
<i class="fas fa-paperclip"></i> Anhang
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<hr>
<h3>Lieferadresse</h3>
<div v-for="field in ['delAddrName', 'delAddrEMail', 'delAddrLine']">{{ order[field] }}</div>
<div>{{ order.delAddrPLZ }} {{ order.delAddrCity }}</div>
</template>
</tt-card>
</div>
`,
props: ['id'],
data: () => ({order: {}, orderLog: null, loading: true}),
props: ['id'],
data() {
return {
order: {},
orderLog: null,
linkedMovements: [],
loading: true,
window: window,
// Status form state
showStatusForm: false,
newStatus: 'noChanges',
selectedLocationId: null,
warehouseLocations: [],
deliveredPositions: {},
uploadedFiles: [],
deliveryNoteFiles: [],
note: '',
isSubmitting: false
};
},
computed: {
grandTotal() {
return this.order.positions?.reduce((sum, p) => sum + (p.amount * p.buyPrice), 0) || 0;
},
statusLabel() {
const labels = {
new: 'Neu',
accepted: 'Akzeptiert',
ordered: 'Bestellt',
sent: 'Versendet',
partiallyDelivered: 'Teilweise geliefert',
fullyDelivered: 'Geliefert',
cancelled: 'Storniert'
};
return labels[this.order.status] || this.order.status;
},
availableStatuses() {
const statusMap = {
new: [
{value: 'noChanges', text: 'Keine Änderungen'},
{value: 'accepted', text: 'Akzeptiert'},
{value: 'ordered', text: 'Bestellt'},
{value: 'cancelled', text: 'Storniert'}
],
accepted: [
{value: 'noChanges', text: 'Keine Änderungen'},
{value: 'ordered', text: 'Bestellt'},
{value: 'cancelled', text: 'Storniert'}
],
ordered: [
{value: 'noChanges', text: 'Keine Änderungen'},
{value: 'sent', text: 'Versendet'},
{value: 'fullyDelivered', text: 'Geliefert'},
{value: 'partiallyDelivered', text: 'Teilweise geliefert'},
{value: 'cancelled', text: 'Storniert'}
],
sent: [
{value: 'noChanges', text: 'Keine Änderungen'},
{value: 'partiallyDelivered', text: 'Teilweise geliefert'},
{value: 'fullyDelivered', text: 'Geliefert'},
{value: 'cancelled', text: 'Storniert'}
],
partiallyDelivered: [
{value: 'noChanges', text: 'Keine Änderungen'},
{value: 'fullyDelivered', text: 'Geliefert'},
{value: 'cancelled', text: 'Storniert'}
]
};
return statusMap[this.order.status] || [{value: 'noChanges', text: 'Keine Änderungen'}];
},
isDeliveryStatus() {
return this.newStatus === 'partiallyDelivered' || this.newStatus === 'fullyDelivered';
},
movementPreviewCount() {
if (!this.deliveredPositions) return 0;
return Object.values(this.deliveredPositions).filter(p => p.amount > 0).length;
},
sortedLog() {
if (!this.orderLog) return [];
return [...this.orderLog].sort((a, b) => b.create - a.create);
}
},
async mounted() {
const [orderResponse, logResponse] = await Promise.all([
const [orderResponse, logResponse, movementsResponse, locationsResponse] = await Promise.all([
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getById`, {params: {id: this.id}}),
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLogById`, {params: {id: this.id}})
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLogById`, {params: {id: this.id}}),
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLinkedMovements`, {params: {orderId: this.id}}),
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLocations`)
]);
this.order = orderResponse.data;
this.orderLog = logResponse.data;
this.linkedMovements = movementsResponse.data || [];
this.warehouseLocations = locationsResponse.data || [];
// Set default location
const defaultLoc = this.warehouseLocations.find(l => l.text === 'K1 Fladnitz 150');
this.selectedLocationId = defaultLoc ? defaultLoc.value : (this.warehouseLocations[0]?.value || null);
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
formatDate: date => window.moment(date * 1000).format('DD.MM.YYYY HH:mm'),
formatCurrency(value) {
return new Intl.NumberFormat('de-AT', { style: 'currency', currency: 'EUR' }).format(value);
},
getUserName: id => window.TT_CONFIG.CRUD_CONFIG.columns.find(col => col.key === 'createBy')?.modal.items.find(u => u.value === id)?.text || 'Unbekannt',
toggleStatusForm() {
this.showStatusForm = !this.showStatusForm;
if (this.showStatusForm) {
this.initDeliveredPositions();
}
},
initDeliveredPositions() {
this.deliveredPositions = {};
if (this.order && this.order.positions) {
this.order.positions.forEach((pos, index) => {
this.$set(this.deliveredPositions, index, {
amount: pos.amount,
reason: '',
cancelRest: false,
articleName: pos.articleName,
orderedAmount: pos.amount
});
});
}
},
async handleFileUpload(event, isDeliveryNote) {
const files = event.target.files;
if (!files.length) return;
for (const file of files) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/uploadFile`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
if (response.data.success) {
const entry = { id: response.data.fileId, name: file.name };
if (isDeliveryNote) {
this.deliveryNoteFiles.push(entry);
} else {
this.uploadedFiles.push(entry);
}
}
} catch (e) {
window.notify('error', 'Fehler beim Hochladen');
}
}
event.target.value = '';
},
cancelStatusChange() {
this.showStatusForm = false;
this.newStatus = 'noChanges';
this.note = '';
this.uploadedFiles = [];
this.deliveryNoteFiles = [];
},
async submitStatusChange() {
this.isSubmitting = true;
try {
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(this.uploadedFiles.map(f => f.id)),
deliveryData: this.isDeliveryStatus ? this.deliveredPositions : null,
locationId: this.selectedLocationId,
deliveryNoteFileIds: this.deliveryNoteFiles.map(f => f.id)
});
if (response.data.success) {
window.notify('success', 'Status erfolgreich geändert');
this.cancelStatusChange();
// Reload data
const [orderRes, logRes, movRes] = await Promise.all([
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getById`, {params: {id: this.id}}),
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLogById`, {params: {id: this.id}}),
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getLinkedMovements`, {params: {orderId: this.id}})
]);
this.order = orderRes.data;
this.orderLog = logRes.data;
this.linkedMovements = movRes.data || [];
// Emit event to refresh table
this.$emit('status-changed');
} else {
window.notify('error', response.data.error || 'Fehler beim Speichern');
}
} catch (e) {
window.notify('error', 'Netzwerkfehler');
}
this.isSubmitting = false;
}
}
});
@@ -642,22 +1039,19 @@ Vue.component('warehouse-order', {
template: `
<tt-card>
<warehouse-order-modal v-if="orderModalId" :id="orderModalId" @close="closeModal"/>
<change-status-modal v-if="changeStatusModalId" :orderId="changeStatusModalId" @close="closeModal"/>
<tt-button text="Bestellung erstellen" @click="orderModalId = 'create'" additional-class="btn-primary"/>
<tt-table-crud emit-edit
@openpdf="openPDF"
@changeStatus="changeStatusModalId = $event.id"
@edit="orderModalId = $event.id; $refs.table.$refs.table.refreshTable()" ref="table">
<template v-slot:expandedRow="{ row }">
<warehouse-order-detail :id="row.id"/>
<warehouse-order-detail :id="row.id" @status-changed="refreshTable"/>
</template>
<template v-slot:sum="{ row }">{{ calculateSum(JSON.parse(row["positions"])).toFixed(2) }} €</template>
</tt-table-crud>
</tt-card>
`,
data: () => ({
orderModalId: null,
changeStatusModalId: null
orderModalId: null
}),
mounted() {
if (JSON.parse(localStorage.getItem('WarehouseOrder_create'))) this.orderModalId = 'create';
@@ -665,10 +1059,12 @@ Vue.component('warehouse-order', {
methods: {
async closeModal() {
this.orderModalId = null;
this.changeStatusModalId = null;
await new Promise(resolve => setTimeout(resolve, 250));
this.$refs.table.$refs.table.refreshTable();
},
refreshTable() {
this.$refs.table.$refs.table.refreshTable();
},
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}`)
}