Files
2025-10-16 08:12:54 +02:00

352 lines
15 KiB
JavaScript

Vue.component('warehouse-offer', {
template: `
<tt-card>
<warehouse-offer-modal v-if="offerModalId" :id="offerModalId" ref="modal"
@close="closeModal"/>
<send-mail-modal v-if="sendMailModalId" :offer-id="sendMailModalId" @close="closeModal"/>
<div class="d-flex" style="gap: 8px; margin-bottom: 1rem;">
<tt-button text="Angebot erstellen" @click="offerModalId = 'create'" additional-class="btn-primary"/>
<div class="dropdown" id="offer-templates-dropdown">
<button class="btn btn-outline-primary dropdown-toggle" @click.stop="offerTemplatesDropdown = !offerTemplatesDropdown" >
Vorlage <i class="fas fa-caret-down"></i>
</button>
<ul class="dropdown-menu" :class="{'show': offerTemplatesDropdown}" @mouseleave="offerTemplatesDropdown = false">
<li v-for="template in offerTemplates" @click="createOfferFromTemplate(template)" style="display: flex; gap: 2px;cursor: pointer;margin-bottom: 4px;margin-right: 4px">
<a class="dropdown-item">{{ template.templateName }}</a>
<tt-button
v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1'"
icon="fas fa-trash" sm additional-class="btn-danger" @click.stop="deleteTemplate(template.id)"/>
</li>
</ul>
</div>
</div>
<tt-table-crud emit-edit
@edit="offerModalId = $event.id"
ref="table"
@openpdf="openPDF($event)"
@sendmail="sendMailModalId = $event.id"
>
<template v-slot:expandedRow="{ row }">
<warehouse-offer-detail :id="row.id"/>
</template>
<template v-slot:totalamount="{ row }">{{ formatPrice(row.totalAmount) }}</template>
<template v-slot:create="{ row }">{{ formatDate(row.create) }}</template>
<template v-slot:lastsentdate="{ row }">{{ formatDate(row.lastSentDate) }}</template>
</tt-table-crud>
</tt-card>
`,
data() {
return {
window: window,
offerModalId: null,
sendMailModalId: null,
offerTemplates: [],
offerTemplatesDropdown: false,
}
},
async mounted() {
await this.loadTemplates();
document.addEventListener('click', this.closeDropdown);
},
beforeDestroy() {
document.removeEventListener('click', this.closeDropdown);
},
methods: {
formatDate: ts => ts ? window.moment(ts * 1000).format('DD.MM.YYYY') : '-',
formatPrice: price => new Intl.NumberFormat('de-AT', { style: 'currency', currency: 'EUR' }).format(price || 0),
async closeModal() {
this.offerModalId = null;
this.sendMailModalId = null;
await new Promise(resolve => setTimeout(resolve, 250));
this.$refs.table.$refs.table.refreshTable();
},
async loadTemplates() {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/getTemplates`);
this.offerTemplates = response.data;
},
openPDF(offer) {
window.open(`${window.TT_CONFIG['BASE_PATH']}/WarehouseOffer/createPDF?id=${offer.id}&version=${offer.version}`)
},
closeDropdown(event) {
if (!event.target.closest('#offer-templates-dropdown')) {
this.offerTemplatesDropdown = false;
}
},
async createOfferFromTemplate(template) {
this.offerTemplatesDropdown = false;
this.offerModalId = 'create';
await this.$nextTick();
this.$refs.modal.offer.positions = JSON.parse(template.positions);
this.$refs.modal.offer.totalDiscount = template.totalDiscount;
this.$refs.modal.offer.paymentTerms = template.paymentTerms;
this.$refs.modal.offer.deliveryTerms = template.deliveryTerms;
this.$refs.modal.offer.closingText = template.closingText;
this.$refs.modal.offer.notes = template.notes;
window.notify('success', 'Angebot aus Vorlage erstellt');
},
async deleteTemplate(id) {
if(!confirm('Vorlage wirklich löschen?')) return;
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/deleteTemplate?id=${id}`);
if (response.data.success) {
await this.loadTemplates();
window.notify('success', 'Vorlage erfolgreich gelöscht');
} else {
window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
}
}
}
});
Vue.component('warehouse-offer-detail', {
template: `
<div class="offer-detail-container">
<div v-if="loading" class="text-center p-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
<div v-else class="offer-detail-grid">
<!-- Left Column: Offer Info -->
<div class="offer-info-pane">
<h5 class="pane-title"><i class="fas fa-info-circle mr-2"></i>Übersicht</h5>
<div class="info-grid">
<div class="info-item">
<label>Kunde</label>
<span>{{ offer.customerName }}</span>
</div>
<div class="info-item">
<label>Zweck</label>
<span>{{ offer.purpose || '-' }}</span>
</div>
<div class="info-item">
<label>Summe</label>
<span>{{ formatPrice(offer.totalAmount) }}</span>
</div>
<div class="info-item">
<label>Status</label>
<span class="badge" :class="statusInfo.badgeClass">{{ statusInfo.text }}</span>
</div>
<div class="info-item">
<label>Sachbearbeiter</label>
<span>{{ editorName }}</span>
</div>
<div class="info-item">
<label>Erstellt am</label>
<span>{{ formatDate(offer.create, 'DD.MM.YYYY') }}</span>
</div>
</div>
</div>
<!-- Right Column: Journal -->
<div class="offer-journal-pane">
<h5 class="pane-title"><i class="fas fa-history mr-2"></i>Journal</h5>
<div class="journal-box mb-3">
<div v-if="journal.length === 0" class="text-center text-muted p-4">Noch keine Einträge vorhanden.</div>
<div v-for="log in journal" class="journal-entry-styled">
<div class="journal-avatar" :style="{ backgroundColor: userColor(log.createByName) }">
<span>{{ log.createByName ? log.createByName.charAt(0) : '?' }}</span>
</div>
<div class="journal-content">
<div class="journal-header">
<strong>{{ log.createByName }}</strong>
<span class="text-muted text-xs ml-auto">{{ formatDate(log.create, 'relative') }}</span>
</div>
<p class="journal-message" v-if="log.message">{{ log.message }}</p>
<div v-if="log.fileIds && JSON.parse(log.fileIds).length > 0" class="mt-2">
<tt-file v-for="fileId in JSON.parse(log.fileIds)" :key="fileId" :id="fileId"/>
</div>
</div>
</div>
</div>
<div class="new-journal-entry">
<tt-textarea v-model="newMessage" placeholder="Neuer Journaleintrag..." no-form-group class="flex-grow-1" rows="2"/>
<div class="new-journal-actions">
<!-- <tt-file-upload-light ref="fileUpload" multiple @uploaded="onFileUploaded" button-text="" /> -->
<tt-button text="Speichern" @click="addJournalEntry" :loading="savingJournal" additional-class="btn-primary" sm/>
</div>
</div>
</div>
</div>
</div>
`,
props: ['id'],
data: () => ({
offer: {},
journal: [],
loading: true,
savingJournal: false,
newMessage: '',
uploadedFiles: [],
userColors: {}
}),
async mounted() {
const [offerResponse, journalResponse] = await Promise.all([
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOffer/getById`, {params: {id: this.id}}),
axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOffer/getJournal`, {params: {id: this.id}})
]);
this.offer = offerResponse.data;
this.journal = journalResponse.data.sort((a,b) => b.create - a.create); // Ensure descending order
this.loading = false;
},
methods: {
formatDate(ts, format = 'DD.MM.YYYY HH:mm') {
if (!ts) return '-';
if (format === 'relative') {
return window.moment(ts * 1000).fromNow();
}
return window.moment(ts * 1000).format(format);
},
formatPrice: price => new Intl.NumberFormat('de-AT', { style: 'currency', currency: 'EUR' }).format(price || 0),
onFileUploaded(file) {
this.uploadedFiles.push(file.id);
},
async addJournalEntry() {
if (!this.newMessage && this.uploadedFiles.length === 0) return;
this.savingJournal = true;
try {
await axios.post(`${window.TT_CONFIG.BASE_PATH}/WarehouseOffer/addJournalEntry`, {
id: this.id,
message: this.newMessage,
fileIds: this.uploadedFiles
});
this.newMessage = '';
this.uploadedFiles = [];
if (this.$refs.fileUpload) this.$refs.fileUpload.reset();
const journalResponse = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOffer/getJournal`, {params: {id: this.id}});
this.journal = journalResponse.data.sort((a,b) => b.create - a.create);
window.notify('success', 'Journaleintrag gespeichert.');
} catch (e) {
window.notify('error', 'Fehler beim Speichern des Eintrags.');
} finally {
this.savingJournal = false;
}
},
userColor(userName) {
if (!userName) return '#cccccc';
if (this.userColors[userName]) return this.userColors[userName];
let hash = 0;
for (let i = 0; i < userName.length; i++) {
hash = userName.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
for (let i = 0; i < 3; i++) {
let value = (hash >> (i * 8)) & 0xFF;
color += ('00' + value.toString(16)).substr(-2);
}
this.userColors[userName] = color;
return color;
}
},
computed: {
editorName() {
const users = window.TT_CONFIG.CRUD_CONFIG.columns.find(c => c.key === 'editor')?.modal?.items || [];
const user = users.find(u => u.value == this.offer.editor);
return user ? user.text : 'Unbekannt';
},
statusInfo() {
const statusMap = {
new: { text: 'Neu', badgeClass: 'badge-primary' },
sent: { text: 'Ausgeschickt', badgeClass: 'badge-info' },
accepted: { text: 'Angenommen', badgeClass: 'badge-success' },
rejected: { text: 'Abgelehnt', badgeClass: 'badge-danger' },
cancelled: { text: 'Storniert', badgeClass: 'badge-secondary' },
};
return statusMap[this.offer.status] || { text: this.offer.status, badgeClass: 'badge-light' };
}
}
});
;
Vue.component('send-mail-modal', {
props: ['offerId'],
template: `
<tt-modal :show="true" title="Angebot per E-Mail senden" @close="$emit('close')" @submit="sendEmail" :save-loading="loading">
<tt-input label="Empfänger E-Mail" v-model="email" required/>
<tt-input label="Betreff" v-model="subject" required/>
<tt-textarea label="Nachricht" v-model="body" rows="5" required/>
</tt-modal>
`,
data() {
return {
loading: false,
email: '',
subject: '',
body: 'Sehr geehrte Damen und Herren,\n\nanbei erhalten Sie das angeforderte Angebot.\n\nMit freundlichen Grüßen\nIhr Team der XINON GmbH'
}
},
async mounted() {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOffer/getById`, {params: {id: this.offerId}});
const offer = response.data;
this.email = offer.contactPersonEmail || '';
this.subject = `Angebot ${offer.offerNumber} von XINON GmbH`;
},
methods: {
async sendEmail() {
this.loading = true;
try {
const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WarehouseOffer/sendOfferEmail`, {
id: this.offerId,
email: this.email,
subject: this.subject,
body: this.body
});
if (response.data.success) {
window.notify('success', response.data.message);
this.$emit('close');
} else {
window.notify('error', response.data.message);
}
} catch (e) {
window.notify('error', 'E-Mail Versand fehlgeschlagen.');
} finally {
this.loading = false;
}
}
}
});
Vue.component('closing-text-modal', {
template: `
<tt-modal :show="true" title="Schlusstext-Vorlage wählen" @close="$emit('close')" :submit-button="false">
<div class="list-group">
<a href="#" v-for="template in templates" :key="template.id" class="list-group-item list-group-item-action" @click.prevent="$emit('select', template.text)">
{{ template.name }}
</a>
</div>
<hr v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1'">
<div v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1'">
<h5>Neue Vorlage erstellen</h5>
<tt-input label="Name" v-model="newTemplate.name" sm/>
<tt-textarea label="Text" v-model="newTemplate.text" sm rows="4"/>
<tt-button text="Vorlage speichern" @click="saveTemplate" sm/>
</div>
</tt-modal>
`,
data() {
return {
templates: [],
newTemplate: { name: '', text: '' },
window: window,
}
},
async mounted() {
await this.loadTemplates();
},
methods: {
async loadTemplates() {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOffer/getClosingTexts`);
this.templates = response.data;
},
async saveTemplate() {
if (!this.newTemplate.name || !this.newTemplate.text) {
return window.notify('error', 'Name und Text dürfen nicht leer sein.');
}
await axios.post(`${window.TT_CONFIG.BASE_PATH}/WarehouseOffer/createClosingText`, this.newTemplate);
this.newTemplate.name = '';
this.newTemplate.text = '';
await this.loadTemplates();
window.notify('success', 'Vorlage gespeichert.');
}
}
});