451 lines
20 KiB
JavaScript
451 lines
20 KiB
JavaScript
Vue.component('tt-resolver', {
|
|
props: {
|
|
value: { type: [Number, String], required: true },
|
|
reference: { type: String, required: true },
|
|
autocomplete: { type: Boolean, default: false }
|
|
},
|
|
data: () => ({loading: true, text: ''}),
|
|
template: `<div v-if="loading" 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>
|
|
<span v-else>{{ text }}</span>`,
|
|
async created() {
|
|
const cacheKey = `${this.reference}_${this.value}_${this.autocomplete}`;
|
|
const cached = JSON.parse(localStorage.getItem(cacheKey) || 'null');
|
|
|
|
if (cached && Date.now() - cached.timestamp < 1800000) {
|
|
this.text = cached.text;
|
|
this.loading = false;
|
|
return;
|
|
}
|
|
|
|
const endpoint = this.autocomplete
|
|
? `${window.TT_CONFIG["BASE_PATH"]}${this.reference}?searchedID=${this.value}`
|
|
: `${window.TT_CONFIG["BASE_PATH"]}/${this.reference}/getById?id=${this.value}`;
|
|
|
|
const { data } = await axios.get(endpoint);
|
|
|
|
if (this.reference === 'WarehouseArticle') this.text = `${data.articleNumber} | ${data.title}`;
|
|
else this.text = this.autocomplete ? data[0]?.text : data.name ?? data.title ?? data.text ?? '[E] Key not found';
|
|
|
|
localStorage.setItem(cacheKey, JSON.stringify({ text: this.text, timestamp: Date.now() }));
|
|
this.loading = false;
|
|
}
|
|
});
|
|
|
|
Vue.component('tt-positions-manager', {
|
|
props: {
|
|
value: {type: [Array, String], required: false},
|
|
config: {type: Object, required: true},
|
|
groupMode: {type: Boolean, default: false},
|
|
submitLoading: {type: Boolean, default: false}
|
|
},
|
|
data() {
|
|
return {
|
|
window: window,
|
|
positions: this.value,
|
|
formData: {},
|
|
groupName: '',
|
|
selectedIndex: null,
|
|
editingGroupName: null,
|
|
tempGroupName: '',
|
|
// New state for instant editing
|
|
editingCell: {
|
|
groupName: null, // For grouped positions
|
|
index: null, // Index within the group or flat array
|
|
key: null // Field key being edited
|
|
}
|
|
}
|
|
},
|
|
template: `
|
|
<div class="positions-manager">
|
|
<template v-if="config['header']">
|
|
<h4 class="text-center">{{ config["header"] }}</h4>
|
|
</template>
|
|
<div class="form-container" ref="formContainer">
|
|
<template v-for="(field, key) in config.fields">
|
|
<template v-if="typeof field.showCondition === 'function' ? field.showCondition(formData) : true">
|
|
<slot :name="key" v-bind:field="field" v-bind:value="formData[key]">
|
|
<div :style="field.style ?? {}" :key="key + field.label">
|
|
<tt-input
|
|
v-if="field.type === 'input'"
|
|
:label="field.label"
|
|
v-model="formData[key]"
|
|
@input="$emit('updateField-' + key, $event)"
|
|
sm
|
|
:type="field.inputType || 'text'"
|
|
/>
|
|
<tt-autocomplete
|
|
v-else-if="field.type === 'autocomplete'"
|
|
:label="field.label"
|
|
:emit-display-value="field.emitDisplayValue || false"
|
|
v-model="formData[key]"
|
|
@input="$emit('updateField-' + key, $event)"
|
|
@displayValue="$emit('updateField-' + key + '_text', $event)"
|
|
:ref="'autocomplete-' + key"
|
|
:api-url="window.TT_CONFIG['BASE_PATH'] + field.apiUrl"
|
|
sm
|
|
/>
|
|
<tt-input-article
|
|
v-if="field.type === 'input-article'"
|
|
:label="field.label"
|
|
:emit-display-value="field.emitDisplayValue || false"
|
|
v-model="formData[key]"
|
|
@input="$emit('updateField-' + key, $event)"
|
|
@displayValue="$emit('updateField-' + key + '_text', $event)"
|
|
:ref="'article-' + key"
|
|
:api-url="field.apiUrl"
|
|
:field="field"
|
|
sm
|
|
/>
|
|
<tt-textarea
|
|
v-else-if="field.type === 'textarea'"
|
|
:label="field.label"
|
|
v-model="formData[key]"
|
|
@input="$emit('updateField-' + key, $event)"
|
|
sm
|
|
/>
|
|
<tt-checkbox
|
|
v-else-if="field.type === 'checkbox'"
|
|
:label="field.label"
|
|
@input="$emit('updateField-' + key, $event)"
|
|
sm
|
|
v-model="formData[key]"
|
|
/>
|
|
<tt-select
|
|
v-else-if="field.type === 'select'"
|
|
:label="field.label"
|
|
@input="$emit('updateField-' + key, $event)"
|
|
sm
|
|
v-model="formData[key]"
|
|
:options="field.options"
|
|
/>
|
|
<slot :name="key + '-prepend'" v-bind:field="field" v-bind:value="formData[key]"/>
|
|
</div>
|
|
|
|
</slot>
|
|
</template>
|
|
</template>
|
|
<div class="button-wrapper">
|
|
<tt-button @click="saveEntry"
|
|
sm
|
|
:loading="submitLoading"
|
|
:additional-class="selectedIndex === null ? 'btn-primary' : 'btn-success'"
|
|
:text="selectedIndex === null ? 'Hinzufügen' : 'Aktualisieren'"/>
|
|
</div>
|
|
|
|
<slot name="form-actions-append"></slot>
|
|
</div>
|
|
|
|
|
|
<div class="form-container" v-if="groupMode">
|
|
<tt-input label="Gruppenname" v-model="groupName" sm/>
|
|
<tt-button @click="addGroup" sm text="Gruppe hinzufügen" additional-class="btn-primary"/>
|
|
</div>
|
|
|
|
<table class="table table-striped table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th v-for="field in config['fields']">{{ field.label }}</th>
|
|
<th style="text-align: right;padding-right: 24px">Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
|
|
<template v-for="(group, groupName) in positionsToRender">
|
|
<tr v-if="groupMode">
|
|
<td :colspan="Object.keys(config.fields).length + 1">
|
|
<div class="d-flex justify-content-center align-items-center">
|
|
<template v-if="editingGroupName === groupName">
|
|
<tt-input v-model="tempGroupName" sm style="max-width: 200px; margin-right: 10px;"/>
|
|
<tt-button @click="saveGroupName(groupName)" sm additional-class="btn-success" icon="fa fa-check" style="margin-right: 5px;"/>
|
|
<tt-button @click="cancelGroupEdit" sm additional-class="btn-secondary" icon="fa fa-times"/>
|
|
</template>
|
|
<template v-else>
|
|
<h4 style="margin: 0; margin-right: 10px;">{{ groupName }}</h4>
|
|
<template v-if="groupName !== 'Keine Gruppe'">
|
|
<tt-button @click="startGroupEdit(groupName)" sm additional-class="btn-primary" icon="fa fa-edit" style="margin-right: 5px;"/>
|
|
<tt-button @click="deleteGroup(groupName)" sm additional-class="btn-danger" icon="fa fa-trash"/>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr v-for="(position, index) in group" :key="groupMode ? groupName + index : index">
|
|
<td v-for="(field, key) in config.fields"
|
|
:class="{'editable-cell': field.editableInTable && (field.type === 'input' || field.type === 'textarea')}"
|
|
@click="startCellEdit(groupName, index, key, field)">
|
|
|
|
<template v-if="isCellEditing(groupName, index, key)">
|
|
<input
|
|
v-if="field.type === 'input'"
|
|
:type="field.inputType || 'text'"
|
|
v-model="position[key]"
|
|
@blur="saveCellEdit(position, key, groupName, index)"
|
|
@keyup.enter="saveCellEdit(position, key, groupName, index)"
|
|
class="form-control form-control-sm"
|
|
autofocus
|
|
/>
|
|
<textarea
|
|
v-else-if="field.type === 'textarea'"
|
|
v-model="position[key]"
|
|
@blur="saveCellEdit(position, key, groupName, index)"
|
|
@keyup.enter.prevent="saveCellEdit(position, key, groupName, index)"
|
|
class="form-control form-control-sm"
|
|
rows="1"
|
|
autofocus
|
|
></textarea>
|
|
</template>
|
|
<template v-else>
|
|
<tt-resolver
|
|
:key="index + key + position[key]"
|
|
v-if="position[key] && (field.customFieldReference || field.type === 'autocomplete')"
|
|
:autocomplete="!field.customFieldReference && field.type === 'autocomplete'"
|
|
:reference="field.customFieldReference || field.apiUrl"
|
|
:value="position[key]"
|
|
/>
|
|
<span v-else-if="field.type === 'checkbox'">{{ position[key] ? 'Ja' : 'Nein' }}</span>
|
|
<span v-else>{{ formatFieldValue(position[key] ?? position[key + '_text'], field) }}</span>
|
|
</template>
|
|
</td>
|
|
<td class="d-flex justify-content-end">
|
|
<select v-if="groupMode" v-model="position._group" @change="$set(position, '_group', $event.target.value)">
|
|
<option v-for="group in allGroups" :value="group">{{ group }}</option>
|
|
</select>
|
|
<tt-button @click="editEntry(getActualIndex(position, groupName, index))" sm additional-class="btn-primary" icon="fa fa-edit"/>
|
|
<tt-button @click="deleteEntry(getActualIndex(position, groupName, index))" sm additional-class="btn-danger" icon="fa fa-trash"/>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
|
|
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`,
|
|
methods: {
|
|
updateField(key, value) {
|
|
this.$set(this.formData, key, value);
|
|
},
|
|
defaultValidateForm(formData) {
|
|
for (const field of this.config["validateFormOptions"]) {
|
|
if (!(formData[field.key] || formData[field.key + '_text'])) {
|
|
window.notify('error', field.message);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
checkEmitDisplayValueAutocomplete() {
|
|
for (const [key, field] of Object.entries(this.config.fields)) {
|
|
if ((typeof field.showCondition === 'function' && field.showCondition(this.formData) === true || !field.showCondition) && field.type === 'autocomplete' && field.emitDisplayValue && (isNaN(this.formData[key]) || !this.formData[key]) && this.$refs['autocomplete-' + key][0]) {
|
|
this.$set(this.formData, key + '_text', this.$refs['autocomplete-' + key][0].displayValue);
|
|
this.$delete(this.formData, key);
|
|
}
|
|
|
|
if ((typeof field.showCondition === 'function' && field.showCondition(this.formData) === true || !field.showCondition) && field.type === 'input-article' && field.emitDisplayValue && (isNaN(this.formData[key]) || !this.formData[key]) && this.$refs['article-' + key][0]) {
|
|
console.log(this.$refs['article-' + key][0].$refs.autocomplete);
|
|
this.$set(this.formData, key + '_text', this.$refs['article-' + key][0].$refs.autocomplete.displayValue);
|
|
this.$delete(this.formData, key);
|
|
}
|
|
|
|
if (field.emitDisplayValue && this.formData[key] !== null && this.formData[key] !== undefined) {
|
|
this.$delete(this.formData, key + '_text');
|
|
}
|
|
|
|
}
|
|
},
|
|
async saveEntry() {
|
|
this.checkEmitDisplayValueAutocomplete();
|
|
if (this.config.hasOwnProperty('validateFormOptions') && !this.defaultValidateForm(this.formData)) return;
|
|
else if (this.config.validateForm && !await this.config.validateForm(this.formData)) return;
|
|
|
|
if (this.selectedIndex === null) this.positions.push(this.formData);
|
|
else this.$set(this.positions, this.selectedIndex, this.formData);
|
|
|
|
if (this.config.customOrdering) {
|
|
this.positions.sort((a, b) => a[this.config.customOrdering] - b[this.config.customOrdering]);
|
|
}
|
|
|
|
this.$emit('input', this.positions);
|
|
this.resetForm();
|
|
},
|
|
addGroup() {
|
|
this.positions.push({_group: this.groupName});
|
|
this.groupName = '';
|
|
},
|
|
startGroupEdit(groupName) {
|
|
this.editingGroupName = groupName;
|
|
this.tempGroupName = groupName;
|
|
},
|
|
saveGroupName(oldGroupName) {
|
|
if (this.tempGroupName.trim() === '') {
|
|
window.notify('error', 'Gruppenname darf nicht leer sein');
|
|
return;
|
|
}
|
|
|
|
if (this.tempGroupName !== oldGroupName) {
|
|
// Update all positions with the old group name to the new group name
|
|
this.positions.forEach(position => {
|
|
if (position._group === oldGroupName) {
|
|
this.$set(position, '_group', this.tempGroupName);
|
|
}
|
|
});
|
|
this.$emit('input', this.positions);
|
|
}
|
|
|
|
this.editingGroupName = null;
|
|
this.tempGroupName = '';
|
|
},
|
|
cancelGroupEdit() {
|
|
this.editingGroupName = null;
|
|
this.tempGroupName = '';
|
|
},
|
|
deleteGroup(groupName) {
|
|
if (confirm(`Möchten Sie die Gruppe "${groupName}" wirklich löschen? Alle Einträge werden in "Keine Gruppe" verschoben.`)) {
|
|
// Move all items from this group to "Keine Gruppe" and remove empty group entries
|
|
this.positions = this.positions.filter(position => {
|
|
if (position._group === groupName) {
|
|
// If it's just a group header (only has _group property), remove it
|
|
if (Object.keys(position).length === 1) {
|
|
return false;
|
|
}
|
|
// Otherwise, move to "Keine Gruppe"
|
|
this.$set(position, '_group', 'Keine Gruppe');
|
|
}
|
|
return true;
|
|
});
|
|
this.$emit('input', this.positions);
|
|
}
|
|
},
|
|
getActualIndex(position, groupName, groupIndex) {
|
|
// Find the actual index in the positions array
|
|
if (!this.groupMode) return groupIndex;
|
|
|
|
let actualIndex = 0;
|
|
let currentGroupIndex = 0;
|
|
|
|
for (let i = 0; i < this.positions.length; i++) {
|
|
const pos = this.positions[i];
|
|
const posGroup = pos._group ?? 'Keine Gruppe';
|
|
|
|
if (posGroup === groupName && Object.keys(pos).length > 1) {
|
|
if (currentGroupIndex === groupIndex) {
|
|
return i;
|
|
}
|
|
currentGroupIndex++;
|
|
}
|
|
}
|
|
|
|
return groupIndex; // Fallback
|
|
},
|
|
async editEntry(index) {
|
|
this.selectedIndex = index;
|
|
for (const [key, field] of Object.entries(this.config.fields)) {
|
|
if (field.type === 'autocomplete' && field.emitDisplayValue) {
|
|
await this.$nextTick();
|
|
if (this.positions[index][key + '_text'] && this.$refs['autocomplete-' + key][0]) {
|
|
console.log('inhere');
|
|
this.$refs['autocomplete-' + key][0].displayValue = this.positions[index][key + '_text'];
|
|
this.$set(this.formData, key, this.positions[index][key]);
|
|
}
|
|
}
|
|
}
|
|
this.formData = {...this.positions[index]};
|
|
|
|
// Scroll to the form container
|
|
this.$nextTick(() => {
|
|
this.$refs.formContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
});
|
|
},
|
|
deleteEntry(index) {
|
|
this.positions.splice(index, 1);
|
|
this.$emit('input', this.positions);
|
|
},
|
|
resetForm() {
|
|
this.formData = {};
|
|
for (const [key, field] of Object.entries(this.config.fields)) {
|
|
if (
|
|
typeof field.showCondition === 'function' && field.showCondition(this.formData) === true &&
|
|
field.type === 'autocomplete' && field.emitDisplayValue && this.$refs['autocomplete-' + key][0]) {
|
|
this.$refs['autocomplete-' + key][0].displayValue = '';
|
|
}
|
|
if (field.inputType === 'date') {
|
|
this.$set(this.formData, key, moment().format('YYYY-MM-DD'));
|
|
}
|
|
}
|
|
this.selectedIndex = null;
|
|
},
|
|
formatFieldValue(value, field) {
|
|
if (field.formatter) return field.formatter(value);
|
|
if (field.inputType === 'date') return moment(value).format('DD.MM.YYYY');
|
|
return value;
|
|
},
|
|
// New methods for instant editing
|
|
startCellEdit(groupName, index, key, field) {
|
|
if (!field.editableInTable || !(field.type === 'input' || field.type === 'textarea')) return;
|
|
this.editingCell = { groupName, index, key };
|
|
this.$nextTick(() => {
|
|
const inputElement = this.$el.querySelector(`.editable-cell input[type="${field.type}"], .editable-cell textarea`);
|
|
if (inputElement) {
|
|
inputElement.focus();
|
|
}
|
|
});
|
|
},
|
|
saveCellEdit(position, key, groupName, index) {
|
|
this.editingCell = { groupName: null, index: null, key: null };
|
|
// Ensure data is updated in the original positions array
|
|
const actualIndex = this.getActualIndex(position, groupName, index);
|
|
this.$set(this.positions, actualIndex, position);
|
|
this.$emit('input', this.positions);
|
|
},
|
|
isCellEditing(groupName, index, key) {
|
|
return this.editingCell.groupName === groupName &&
|
|
this.editingCell.index === index &&
|
|
this.editingCell.key === key;
|
|
}
|
|
},
|
|
//TODO: cleanup
|
|
created() {
|
|
if (this.config["customMethods"]) Object.assign(this, this.config.customMethods);
|
|
if (!this.positions) this.positions = [];
|
|
if (typeof this.positions === 'string') {
|
|
try {
|
|
this.positions = JSON.parse(this.positions);
|
|
} catch (e) {
|
|
console.error(e);
|
|
this.positions = [];
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
this.resetForm();
|
|
},
|
|
computed: {
|
|
groupedPositions() {
|
|
const groups = {};
|
|
for (const position of this.positions) {
|
|
const group = position._group ?? 'Keine Gruppe';
|
|
if (!groups[group]) groups[group] = [];
|
|
if (Object.keys(position).length !== 1) groups[group].push(position);
|
|
}
|
|
return groups;
|
|
},
|
|
positionsToRender() {
|
|
return this.groupMode ? this.groupedPositions : {'': this.positions};
|
|
},
|
|
allGroups() {
|
|
return Object.keys(this.groupedPositions);
|
|
}
|
|
},
|
|
watch: {
|
|
value: {
|
|
handler() {
|
|
this.positions = this.value;
|
|
},
|
|
deep: true
|
|
}
|
|
}
|
|
}); |