Files
thetool/public/js/pages/WarehouseShippingNote/WarehouseShippingNoteModal.js
2025-09-01 11:32:12 +02:00

336 lines
17 KiB
JavaScript

// noinspection EqualityComparisonWithCoercionJS
Vue.component('warehouse-shipping-note-modal', {
props: {
id: {type: [String, Number], required: true},
},
data() {
return {
window: window,
loading: false,
addressEditMode: false,
billAddrAutoCompleteUrl: window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress&fibu_primary_account=1',
geoAutocompleteUrl: window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/geoAutocomplete',
shippingNote: {
billingAddressId: null,
deliveryAddressName: '',
deliveryAddressEMail: '',
deliveryAddressLine: '',
deliveryAddressPLZ: '',
deliveryAddressCity: '',
status: 'new',
metadata: null,
positions: [],
textElements: [],
hoursEntries: [],
},
availableTypes: [
{text: 'Xinon Intern (XI)', value: 'XI'},
{text: 'Xinon Hersteller (XH)', value: 'XH'},
{text: 'Energie Steiermark (ESTMK)', value: 'ESTMK'},
{text: 'Steirische Breitband- und Digitalinfrastrukturgesellschaft (SBIDI)', value: 'SBIDI'},
{text: 'Verrechnen (V)', value: 'V'},
],
hoursLoading: false,
geoAddr: '',
selectedBillingAddress: '',
positionsConfig: {
customOrdering: 'article',
fields: {
article: {
type: 'input-article',
label: 'Artikel',
apiUrl: '/WarehouseArticle/autoComplete',
customFieldReference: 'WarehouseArticle',
emitDisplayValue: true,
},
amount: {type: 'input', label: 'Menge', inputType: 'number'},
isEnergieMaterial: {type: 'checkbox', label: 'Beigestelltes Material'},
},
validateFormOptions: [
{key: 'article', message: 'Bitte füllen Sie den Artikel aus'},
{key: 'amount', message: 'Bitte füllen Sie die Menge aus'},
]
},
hoursConfig: {
fields: {
userId: {
type: 'autocomplete',
label: 'Mitarbeiter',
apiUrl: '/WarehouseShippingNote/userAutoComplete',
emitDisplayValue: true,
},
date: {type: 'input', label: 'Datum', inputType: 'date'},
hourCount: {type: 'input', label: 'Stunden', inputType: 'number'},
carId: {
type: 'autocomplete',
label: 'Auto',
apiUrl: '/WarehouseShippingNote/timerecordingCarAutoComplete',
emitDisplayValue: true,
showCondition: (formData) => {
return !isNaN(parseInt(formData.userId))
},
},
externalCar: {
type: 'checkbox',
label: 'Externes Auto',
showCondition: (formData) => {
return isNaN(parseInt(formData.userId)) || !formData.userId
},
},
kilometerCount: {type: 'input', label: 'Kilometer', inputType: 'number'},
hourType: {type: 'select', label: 'Stundenart', options: [
{text: 'Normal', value: undefined},
{text: '+50%', value: '50'},
{text: '+100%', value: '100'},
{text: 'Regie', value: 'regie'},
]},
comment: {type: 'input', label: 'Kommentar'},
},
validateFormOptions: [
{key: 'userId', message: 'Bitte füllen Sie den Mitarbeiter aus'},
{key: 'hourCount', message: 'Bitte füllen Sie die Stunden aus'},
]
}
}
},
//language=Vue
template: `
<tt-modal :show="true"
:delete="false"
:title="id === 'create' ? 'Lieferschein erstellen' : 'Lieferschein bearbeiten'"
@update:show="$emit('close')"
@submit="submit"
:save-loading="loading"
>
<div style="width: 99%">
<h4 class="text-center">Adresse</h4>
<tt-autocomplete :api-url="geoAutocompleteUrl" label="Adresse suchen" sm row v-model="geoAddr"/>
<tt-autocomplete :api-url="billAddrAutoCompleteUrl" label="Kunde suchen" sm row v-model="selectedBillingAddress"/>
<hr>
<tt-input v-model="shippingNote.deliveryAddressName" label="Name" sm row/>
<tt-input v-model="shippingNote.deliveryAddressEMail" label="E-Mail" sm row/>
<div v-if="!addressEditMode" class="d-flex align-items-start">
<div class="mb-1">
<div v-if="shippingNote.deliveryAddressLine">
{{ shippingNote.deliveryAddressLine }}<br>
{{ shippingNote.deliveryAddressPLZ }} {{ shippingNote.deliveryAddressCity }}
</div>
</div>
<button @click="addressEditMode = true" class="btn btn-sm btn-link ml-2">
<i class="fas fa-edit"></i>
</button>
</div>
<!-- Edit form -->
<div v-if="addressEditMode" class="address-edit-form mt-2">
<div style="display:grid; grid-template-columns: 3fr 1.5fr 1.5fr 1fr; gap: 10px">
<tt-input
label="Adresse"
type="text"
placeholder="Adresse"
v-model="shippingNote.deliveryAddressLine"
sm
></tt-input>
<tt-input
label="PLZ"
type="text"
placeholder="PLZ"
v-model="shippingNote.deliveryAddressPLZ"
sm
no-form-group
></tt-input>
<tt-input
label="Stadt"
type="text"
placeholder="Stadt"
v-model="shippingNote.deliveryAddressCity"
sm
no-form-group
></tt-input>
<button @click="addressEditMode = false" class="btn btn-sm btn-link">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<template v-if="window.TT_CONFIG['WAREHOUSE_ADMIN'] == true">
<hr>
<tt-autocomplete v-model="shippingNote.billingAddressId" :api-url="billAddrAutoCompleteUrl" label="Rechnungsadresse" sm row/>
<tt-autocomplete v-model="shippingNote.type" :items="availableTypes" label="Typ" sm row return-text case-insensitive/>
</template>
<tt-textarea v-model="shippingNote.note" label="Art der Arbeit" sm row/>
<template v-if="shippingNote.deliveryAddressLine">
<hr>
<h4 class="text-center">Stunden</h4>
<tt-positions-manager
ref="hoursManager"
:submit-loading="hoursLoading"
@updateField-userId="updateCarId"
@updateField-carId="updateKilometer"
@updateField-externalCar="updateKilometer"
:config="hoursConfig" v-model="shippingNote.hoursEntries" sm row/>
<hr>
<h4 class="text-center">Positionen</h4>
<tt-positions-manager :config="positionsConfig" v-model="shippingNote.positions" sm row ref="positionsManager"/>
</template>
<div v-else class="text-danger text-center font-weight-bolder font-16">Bitte Lieferadresse eingeben oder nach Kunden suchen</div>
</div>
<template v-slot:footer-prepend v-if="id !== 'create' && !$slots['footer-prepend']">
<button class="btn btn-info" @click="$emit('open-signing-modal', id)">Unterschreiben</button>
</template>
<template v-slot:footer-prepend v-else>
<slot name="footer-prepend"/>
</template>
</tt-modal>
`,
async mounted() {
if (this.id === 'create') return;
const {data} = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/getById`, {params: {id: this.id}});
this.shippingNote = {
...data,
positions: JSON.parse(data.positions),
hoursEntries: JSON.parse(data.hoursEntries),
metadata: data.metadata ? JSON.parse(data.metadata) : null
};
},
watch: {
geoAddr: async function() {
if (!this.geoAddr) return;
const areaMode = this.geoAddr.includes(', area');
const [lat, lon] = this.geoAddr.replace(', area', '').split(',');
const { address } = (await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/geoReverse?lat=${lat}&lon=${lon}`)).data;
if (areaMode === false) {
const addrKey = ['road', 'village', 'hamlet', 'residential', 'city'].find(k => address[k]);
this.shippingNote.deliveryAddressLine = addrKey ? `${address[addrKey]}${address["house_number"] ? ` ${address["house_number"]}` : ''}` : '';
} else if (areaMode === true) {
this.shippingNote.deliveryAddressLine = 'Gebiet';
this.shippingNote.deliveryAddressName = `Gebiet ${address["village"] ?? address.residential ?? address.city ?? address["town"]}`;
}
this.shippingNote.deliveryAddressPLZ = address["postcode"];
this.shippingNote.deliveryAddressCity = address["village"] ?? address.residential ?? address.city ?? address["town"];
this.updateKilometer().then();
},
selectedBillingAddress: async function() {
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/Address/api?do=getAddress&id=' + this.selectedBillingAddress);
if (response.data.status !== 'OK' || !response.data.result.address) {
this.window.notify('error', 'Rechnungsadresse konnte nicht gefunden werden');
return;
}
this.shippingNote.deliveryAddressName = response.data.result.address["company"] || response.data.result.address.firstname + ' ' + response.data.result.address.lastname;
this.shippingNote.deliveryAddressLine = response.data.result.address.street;
this.shippingNote.deliveryAddressPLZ = response.data.result.address.zip;
this.shippingNote.deliveryAddressCity = response.data.result.address.city;
this.shippingNote.deliveryAddressEMail = response.data.result.address.email;
}
},
methods: {
async submit(newStatus = null) {
this.loading = true;
if (!this.shippingNote.positions.length && !this.shippingNote.hoursEntries.length) {
this.loading = false;
return window.notify('error', 'Mindestens eine Position oder eine Stundenbuchung sind erforderlich');
}
if (this.availableTypes.find(t => t.text === this.shippingNote.type)) {
this.shippingNote.type = this.availableTypes.find(t => t.name === this.shippingNote.type).value;
}
if (newStatus !== null) {
this.shippingNote.status = newStatus;
}
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/${this.id === 'create' ? 'create' : 'update'}`, this.shippingNote);
if ((this.id !== 'create' && response.data.success === true) || response.data.success === true) this.$emit('close');
window.notify(response.data.success ? 'success' : 'error',
response.data.success
? this.id === 'create' ? response.data.message : (response.data.message ?? 'Bestellung erfolgreich aktualisiert')
: (response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten')
);
this.loading = false;
},
async updateCarId(userId) {
if (!userId) return this.$refs.hoursManager.updateField('carId', null);
this.hoursLoading = true;
const {data} = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/timerecordingCarForUser?userId=' + userId);
if (data.status === 'USER_NO_CAR') this.$refs.hoursManager.updateField('carId', null);
else this.$refs.hoursManager.updateField('carId', data.id);
this.hoursLoading = false;
},
async updateKilometer(carId) {
if (!carId || carId === '0' && this.$refs.hoursManager) return this.$refs.hoursManager?.updateField('kilometerCount', null);
this.hoursLoading = true;
const delAddr = this.shippingNote.deliveryAddressLine + ' ' + this.shippingNote.deliveryAddressPLZ + ' ' + this.shippingNote.deliveryAddressCity;
const {data} = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getDistance?from=Xinon%20GmbH&to=' + delAddr);
this.$refs.hoursManager.updateField('kilometerCount', data.distance);
this.hoursLoading = false;
}
}
})
Vue.component('warehouse-shipping-note-signature-pad', {
props: {
shippingNoteId: {type: Number, required: true}
},
data() {
return {
window: window,
signaturePad: null,
shippingNote: null,
signatureName: '',
}
},
//language=Vue
template: `
<tt-modal class="signModal" :show="true" :delete="false" :submit="false" @update:show="$emit('close')" :title="'Unterschrift'">
<div style="max-width: 520px;display: flex; flex-direction: column; align-items: center;">
<div style="width: 480px">
<tt-input v-model="signatureName" label="Name" row/>
</div>
<div>
<canvas id="signature-pad" width="500" height="200" style="border: 1px solid black"></canvas>
</div>
<div>
<button class="btn btn-primary" @click="submit()">Speichern</button>
<button class="btn btn-primary" @click="signaturePad.clear()">Leeren</button>
</div>
</div>
</tt-modal>
`,
methods: {
async submit() {
const data = this.signaturePad.toDataURL();
const response = await axios.post(window.TT_CONFIG['BASE_PATH'] + '/WarehouseShippingNote/sign?id=' + this.shippingNoteId,
{signature: data, signatureName: this.signatureName});
if (response.data.success) {
this.window.notify('success', response.data.message || 'Erfolgreich unterschrieben');
this.$emit('close');
} else {
this.window.notify('error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
}
},
},
async mounted() {
// fetch shipping note by id
const response = await axios.get(window.TT_CONFIG["BASE_PATH"] + '/WarehouseShippingNote/getById?id=' + this.shippingNoteId);
this.shippingNote = response.data;
this.signaturePad = new SignaturePad(document.getElementById('signature-pad'));
}
})