added technical data to xinon workorder and workordermph now has a unassign button
This commit is contained in:
@@ -161,9 +161,18 @@ Vue.component('manual-invoice-modal', {
|
||||
<tt-autocomplete label="Kunde suchen" :api-url="customerApiUrl" v-model="invoiceData.billingaddress_id" sm row />
|
||||
</tt-card>
|
||||
<tt-card><template v-slot:header><h5><i class="fas fa-file-invoice mr-2"></i>Rechnungsdetails</h5></template>
|
||||
<div class="form-grid">
|
||||
<tt-input label="Rechnungsdatum" type="date" v-model="invoiceData.invoice_date" sm/>
|
||||
<tt-select label="Zahlungsart" v-model="invoiceData.billing_type" :options="billingTypeOptions" sm/>
|
||||
<div class="form-row mb-2">
|
||||
<div class="col-md-6">
|
||||
<label class="small text-muted">Zahlungsart</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<span :class="['badge', effectiveBillingType === 'sepa' ? 'badge-info' : 'badge-secondary']">
|
||||
{{ effectiveBillingType === 'sepa' ? 'SEPA' : 'Rechnung' }}
|
||||
</span>
|
||||
<small v-if="customerBillingInfo.billing_type === 'sepa' && effectiveBillingType === 'invoice'" class="text-warning ml-2">
|
||||
<i class="fas fa-exclamation-triangle"></i> Brutto überschreitet SEPA-Limit ({{ formatPrice(customerBillingInfo.manual_invoice_sepa_limit) }})
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<tt-input label="Leistungszeitraum" v-model="invoiceData.leistungszeitraum" sm row placeholder="z.B. 01.01.2025 - 31.01.2025"/>
|
||||
<tt-input label="Externe Referenz" v-model="invoiceData.externe_referenz" sm row placeholder="z.B. Auftragsnummer, Bestellnummer"/>
|
||||
@@ -172,9 +181,8 @@ Vue.component('manual-invoice-modal', {
|
||||
<tt-card><template v-slot:header><h5><i class="fas fa-list-ol mr-2"></i>Positionen</h5></template>
|
||||
<tt-positions-manager group-mode ref="positionsManager" v-model="invoiceData.positions" :config="positionsConfig" @updateField-article_id="onArticleSelected" />
|
||||
</tt-card>
|
||||
<tt-card><template v-slot:header><h5><i class="fas fa-paragraph mr-2"></i>Texte & Rabatt</h5></template>
|
||||
<tt-card><template v-slot:header><h5><i class="fas fa-paragraph mr-2"></i>Rabatt</h5></template>
|
||||
<tt-input label="Gesamtrabatt (%)" v-model.number="invoiceData.gesamtrabatt" sm row type="number" placeholder="0"/>
|
||||
<tt-textarea label="Steuerhinweis" v-model="invoiceData.tax_text" rows="2"/>
|
||||
</tt-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,6 +205,12 @@ Vue.component('manual-invoice-modal', {
|
||||
pdfLoading: false,
|
||||
pdfPreviewUrl: '',
|
||||
previewDebounceTimer: null,
|
||||
customerBillingInfo: {
|
||||
billing_type: 'invoice',
|
||||
manual_invoice_sepa_limit: null,
|
||||
vatarea: 'domestic',
|
||||
tax_text: ''
|
||||
},
|
||||
invoiceData: {
|
||||
id: null, invoice_number: null, invoice_date: moment().format('YYYY-MM-DD'),
|
||||
billingaddress_id: null, owner_id: null, customer_number: 0, fibu_account_number: 0,
|
||||
@@ -205,7 +219,6 @@ Vue.component('manual-invoice-modal', {
|
||||
leistungszeitraum: '', einleitender_text: '', externe_referenz: '', gesamtrabatt: 0,
|
||||
positions: [], total: 0, total_gross: 0
|
||||
},
|
||||
billingTypeOptions: [{value: 'invoice', text: 'Rechnung'}, {value: 'sepa', text: 'SEPA'}],
|
||||
positionsConfig: {
|
||||
fields: {
|
||||
article_id: {
|
||||
@@ -270,16 +283,31 @@ Vue.component('manual-invoice-modal', {
|
||||
});
|
||||
|
||||
return { subtotal, net, vat, gross };
|
||||
},
|
||||
effectiveBillingType() {
|
||||
if (this.customerBillingInfo.billing_type !== 'sepa') return 'invoice';
|
||||
if (this.customerBillingInfo.manual_invoice_sepa_limit === null) return 'sepa';
|
||||
return this.totals.gross <= this.customerBillingInfo.manual_invoice_sepa_limit ? 'sepa' : 'invoice';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'invoiceData': { handler() { this.debouncedPreviewUpdate(); }, deep: true },
|
||||
effectiveBillingType: {
|
||||
handler(newType) {
|
||||
this.invoiceData.billing_type = newType;
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
'invoiceData.billingaddress_id': {
|
||||
async handler(newId) {
|
||||
if (!newId) return Object.assign(this.invoiceData, {
|
||||
company: '', firstname: '', lastname: '', street: '', zip: '', city: '',
|
||||
country: 'Österreich', uid: '', email: '', customer_number: 0, fibu_account_number: 0, owner_id: 0
|
||||
});
|
||||
if (!newId) {
|
||||
Object.assign(this.invoiceData, {
|
||||
company: '', firstname: '', lastname: '', street: '', zip: '', city: '',
|
||||
country: 'Österreich', uid: '', email: '', customer_number: 0, fibu_account_number: 0, owner_id: 0
|
||||
});
|
||||
this.customerBillingInfo = { billing_type: 'invoice', manual_invoice_sepa_limit: null, vatarea: 'domestic', tax_text: '' };
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/Address/api?do=getAddress&id=${newId}`);
|
||||
if (data.status === 'OK' && data.result.address) {
|
||||
@@ -291,6 +319,8 @@ Vue.component('manual-invoice-modal', {
|
||||
fibu_account_number: a.fibu_account_number || 0, owner_id: newId
|
||||
});
|
||||
}
|
||||
|
||||
await this.fetchCustomerBillingInfo(newId);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -327,10 +357,35 @@ Vue.component('manual-invoice-modal', {
|
||||
methods: {
|
||||
close() { this.$emit('close'); },
|
||||
saveInvoice() {
|
||||
this.invoiceData.invoice_date = moment().format('YYYY-MM-DD');
|
||||
this.invoiceData.billing_type = this.effectiveBillingType;
|
||||
this.invoiceData.tax_text = this.customerBillingInfo.tax_text;
|
||||
if (!this.invoiceData.billingaddress_id) return window.notify('error', 'Bitte wählen Sie einen Kunden aus.');
|
||||
if (!this.invoiceData.positions?.length) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
|
||||
this.$emit('save', this.invoiceData);
|
||||
},
|
||||
formatPrice(value) {
|
||||
if (value === null || value === undefined) return '-';
|
||||
return new Intl.NumberFormat('de-AT', { style: 'currency', currency: 'EUR' }).format(value);
|
||||
},
|
||||
async fetchCustomerBillingInfo(addressId) {
|
||||
if (!addressId) return;
|
||||
try {
|
||||
const vatgroupId = this.invoiceData.vatgroup_id || 2;
|
||||
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/getCustomerBillingInfo?address_id=${addressId}&vatgroup_id=${vatgroupId}`);
|
||||
if (data.success) {
|
||||
this.customerBillingInfo = {
|
||||
billing_type: data.billing_type || 'invoice',
|
||||
manual_invoice_sepa_limit: data.manual_invoice_sepa_limit,
|
||||
vatarea: data.vatarea || 'domestic',
|
||||
tax_text: data.tax_text || ''
|
||||
};
|
||||
this.invoiceData.tax_text = data.tax_text || '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching customer billing info:', e);
|
||||
}
|
||||
},
|
||||
handleResize() { this.isLargeScreen = window.innerWidth >= 1920; },
|
||||
handleGlobalKeydown(e) {
|
||||
if (e.ctrlKey && e.key === 'q') { e.preventDefault(); this.togglePreviewVisibility(); }
|
||||
@@ -339,9 +394,9 @@ Vue.component('manual-invoice-modal', {
|
||||
async onArticleSelected(articleId) {
|
||||
if (!articleId) return;
|
||||
try {
|
||||
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/getArticleVatInfo?article_id=${articleId}`);
|
||||
const vatarea = this.customerBillingInfo.vatarea || 'domestic';
|
||||
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/getArticleVatInfo?article_id=${articleId}&vatarea=${vatarea}`);
|
||||
if (data.success && this.$refs.positionsManager) {
|
||||
// Update the formData in the positions manager
|
||||
const pm = this.$refs.positionsManager;
|
||||
if (data.article) {
|
||||
pm.$set(pm.formData, 'product_name', data.article.title);
|
||||
@@ -351,13 +406,26 @@ Vue.component('manual-invoice-modal', {
|
||||
pm.$set(pm.formData, 'fibu_cost_account', data.fibu_cost_account);
|
||||
pm.$set(pm.formData, 'fibu_cost_account_legacy', data.fibu_cost_account_legacy);
|
||||
pm.$set(pm.formData, 'fibu_taxcode', data.fibu_taxcode);
|
||||
// Store vatgroup_id on invoice level if needed
|
||||
this.invoiceData.vatgroup_id = data.vatgroup_id;
|
||||
await this.updateTaxText(data.vatgroup_id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching article VAT info:', e);
|
||||
}
|
||||
},
|
||||
async updateTaxText(vatgroupId) {
|
||||
if (!vatgroupId) return;
|
||||
try {
|
||||
const vatarea = this.customerBillingInfo.vatarea || 'domestic';
|
||||
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/getTaxText?vatgroup_id=${vatgroupId}&vatarea=${vatarea}`);
|
||||
if (data.success) {
|
||||
this.customerBillingInfo.tax_text = data.tax_text || '';
|
||||
this.invoiceData.tax_text = data.tax_text || '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching tax text:', e);
|
||||
}
|
||||
},
|
||||
debouncedPreviewUpdate() {
|
||||
clearTimeout(this.previewDebounceTimer);
|
||||
this.previewDebounceTimer = setTimeout(() => this.updatePdfPreview(), 2000);
|
||||
|
||||
@@ -273,7 +273,44 @@ Vue.component('workorder-details-manager', {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="showTechnicalData && technicalData && (technicalData.patchposition?.equipmentName || technicalData.rimoWorkorders?.length)" class="card mb-3">
|
||||
<div class="card-header bg-purple text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-microchip mr-2"></i>Technische Daten</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6" v-if="technicalData.patchposition?.equipmentName">
|
||||
<h6>Patchposition</h6>
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<tr>
|
||||
<th class="border-top-0">Equipment Name:</th>
|
||||
<td class="border-top-0 text-monospace">{{ technicalData.patchposition.equipmentName }}</td>
|
||||
</tr>
|
||||
<tr v-if="technicalData.patchposition.equipmentPort">
|
||||
<th>Equipment Port:</th>
|
||||
<td class="text-monospace">{{ technicalData.patchposition.equipmentPort }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6" v-if="technicalData.rimoWorkorders?.length">
|
||||
<h6>AHA Blätter</h6>
|
||||
<div v-for="wo in technicalData.rimoWorkorders" :key="wo.id" class="mb-2">
|
||||
<div class="d-flex align-items-center justify-content-between border rounded p-2">
|
||||
<div>
|
||||
<strong>{{ wo.rimoName }}</strong>
|
||||
<small class="text-muted ml-2">{{ wo.rimoStatus }}</small>
|
||||
</div>
|
||||
<a :href="wo.downloadUrl" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-file-pdf mr-1"></i> AHA Blatt
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3" v-if="isAdmin && selectedDocs.length > 0">
|
||||
<div class="card-header bg-warning"><h5><i class="fas fa-exclamation-triangle mr-2"></i>Korrektur anfordern</h5></div>
|
||||
<div class="card-body">
|
||||
@@ -328,6 +365,9 @@ Vue.component('workorder-details-manager', {
|
||||
requireCableLength: false,
|
||||
requireCableType: false,
|
||||
savingData: false,
|
||||
// Technical data
|
||||
showTechnicalData: false,
|
||||
technicalData: null,
|
||||
// Admin state
|
||||
selectedDocs: [], correctionText: '', correctionLoading: false, showAcceptModal: false, showRevertModal: false,
|
||||
}),
|
||||
@@ -394,6 +434,8 @@ Vue.component('workorder-details-manager', {
|
||||
this.interventionTypes = data.interventionTypes;
|
||||
this.requireCableLength = data.requireCableLength || false;
|
||||
this.requireCableType = data.requireCableType || false;
|
||||
this.showTechnicalData = data.showTechnicalData || false;
|
||||
this.technicalData = data.technicalData || null;
|
||||
}
|
||||
} catch (e) { console.error("Mandantenkonfiguration nicht geladen", e); }
|
||||
finally { this.loadingConfig = false; }
|
||||
|
||||
@@ -30,10 +30,13 @@ Vue.component('workorder-mph-admin', {
|
||||
</div>
|
||||
<div v-else><span>{{ row.companyName || 'N/A' }}</span></div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(2, auto); gap: 0px; padding-left: 8px;">
|
||||
<tt-button v-if="!['completed', 'new'].includes(row.status)" icon="fas fa-edit"
|
||||
<div style="display: grid; grid-template-columns: repeat(3, auto); gap: 0px; padding-left: 8px;">
|
||||
<tt-button v-if="!['completed', 'new', 'cancelled'].includes(row.status)" icon="fas fa-edit"
|
||||
@click="startCompanyEdit(row)" additional-class="btn-link workorder-mph-button"
|
||||
title="Zuweisung ändern"/>
|
||||
<tt-button v-if="!['completed', 'new', 'cancelled'].includes(row.status)" icon="fas fa-user-slash text-warning"
|
||||
@click="unassignWorkorderModalData = row" additional-class="btn-link workorder-mph-button"
|
||||
title="Zuweisung aufheben"/>
|
||||
<tt-button v-if="!['completed', 'cancelled'].includes(row.status)" icon="fas fa-ban text-danger"
|
||||
@click="cancelWorkorderModalData = row" additional-class="btn-link workorder-mph-button"
|
||||
title="Auftrag stornieren"/>
|
||||
@@ -101,6 +104,13 @@ Vue.component('workorder-mph-admin', {
|
||||
<p>Soll der Auftrag <strong>#{{ cancelWorkorderModalData.id }}</strong> wirklich storniert werden?</p>
|
||||
<tt-textarea label="Grund (optional)" v-model="cancelWorkorderModalData.reason" sm row/>
|
||||
</tt-modal>
|
||||
|
||||
<tt-modal v-if="unassignWorkorderModalData" :show.sync="unassignWorkorderModalData"
|
||||
title="Zuweisung aufheben" @submit="unassignWorkorder">
|
||||
<p>Soll die Zuweisung für Auftrag <strong>#{{ unassignWorkorderModalData.id }}</strong> aufgehoben werden?</p>
|
||||
<p class="text-muted small">Aktuell zugewiesen an: <strong>{{ unassignWorkorderModalData.companyName }}</strong></p>
|
||||
<tt-textarea label="Grund (optional)" v-model="unassignWorkorderModalData.reason" sm row/>
|
||||
</tt-modal>
|
||||
</tt-card>
|
||||
`,
|
||||
data() {
|
||||
@@ -113,6 +123,7 @@ Vue.component('workorder-mph-admin', {
|
||||
companies: [],
|
||||
companiesLoading: false,
|
||||
cancelWorkorderModalData: null,
|
||||
unassignWorkorderModalData: null,
|
||||
crudConfig: {
|
||||
...window.TT_CONFIG.CRUD_CONFIG,
|
||||
selectable: false,
|
||||
@@ -237,6 +248,20 @@ Vue.component('workorder-mph-admin', {
|
||||
} else {
|
||||
window.notify('error', data.message || 'Stornierung fehlgeschlagen.');
|
||||
}
|
||||
},
|
||||
async unassignWorkorder() {
|
||||
const { id, reason } = this.unassignWorkorderModalData;
|
||||
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphAdmin/unassignWorkorder`, {
|
||||
workorderId: id,
|
||||
reason: reason
|
||||
});
|
||||
if (data.success) {
|
||||
window.notify('success', data.message);
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
this.unassignWorkorderModalData = null;
|
||||
} else {
|
||||
window.notify('error', data.message || 'Aufheben der Zuweisung fehlgeschlagen.');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -89,6 +89,8 @@ Vue.component('workorder-tenant-config', {
|
||||
v-model="editableItem.requireCableLength" sm/>
|
||||
<tt-checkbox label="Kabeltyp erforderlich"
|
||||
v-model="editableItem.requireCableType" sm/>
|
||||
<tt-checkbox label="Technische Daten anzeigen (Patchposition, AHA Blatt)"
|
||||
v-model="editableItem.showTechnicalData" sm/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>Workorder: <strong>{{ config.enableWorkorder ? 'Aktiviert' : 'Deaktiviert' }}</strong></p>
|
||||
@@ -97,6 +99,7 @@ Vue.component('workorder-tenant-config', {
|
||||
<p>Tiefbau-Doku: <strong>{{ config.civilEngineeringDocsRequired ? 'Ja' : 'Nein' }}</strong></p>
|
||||
<p>Kabellänge-Doku: <strong>{{ config.requireCableLength ? 'Ja' : 'Nein' }}</strong></p>
|
||||
<p>Kabeltyp-Doku: <strong>{{ config.requireCableType ? 'Ja' : 'Nein' }}</strong></p>
|
||||
<p>Technische Daten: <strong>{{ config.showTechnicalData ? 'Ja' : 'Nein' }}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@@ -333,6 +336,7 @@ Vue.component('workorder-tenant-config', {
|
||||
civilEngineeringDocsRequired: 0,
|
||||
requireCableLength: 0,
|
||||
requireCableType: 0,
|
||||
showTechnicalData: 0,
|
||||
enableWorkorder: 1,
|
||||
enableWorkorderMph: 1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user