Merge branch 'Warehouse/improve' into 'master'
fixed getting wrong order results See merge request fronk/thetool!1320
This commit is contained in:
@@ -9,7 +9,7 @@ class WarehouseArticleController extends TTCrud {
|
||||
protected array $columns = [
|
||||
['key' => 'title', 'text' => 'Titel', 'required' => true, 'table' => ['priority' => 9]],
|
||||
['key' => 'articleNumber', 'text' => 'Nr.', 'required' => true],
|
||||
['key' => 'description', 'text' => 'Beschreibung', 'required' => true],
|
||||
['key' => 'description', 'text' => 'Beschreibung', 'required' => true, 'table' => ['sortable' => false]],
|
||||
['key' => 'category_id', 'text' => 'Kategorie', 'required' => true, 'modal' => ['type' => 'select', 'items' => []], 'table' => ['filter' => 'select']],
|
||||
['key' => 'unit', 'text' => 'Einheit', 'required' => true,'table' => false],
|
||||
['key' => 'revenueAccount', 'text' => 'Erlöskonto', 'required' => true,'modal' => ['type' => 'select', 'items' => [['value' => 0, 'text' => 'Dienstleistungen'], ['value' => 1, 'text' => 'Handelswaren']]], 'table' => false],
|
||||
|
||||
@@ -2,6 +2,8 @@ async function handleApiResponse(responsePromise) {
|
||||
const res = await responsePromise;
|
||||
if (res.data.success === false) return window.notify('error', `Fehler: ${res.data.errors.join(', ')}`);
|
||||
window.notify('success', res.data.message || 'Erfolgreich');
|
||||
|
||||
window.dispatchEvent(new Event('refreshTable'));
|
||||
}
|
||||
|
||||
Vue.component('warehouse-article-prices', {
|
||||
@@ -148,6 +150,16 @@ Vue.component('warehouse-article-distributor', {
|
||||
Vue.component('warehouse-article', {
|
||||
template: `
|
||||
<tt-card>
|
||||
<tt-select
|
||||
multiple
|
||||
sm
|
||||
row
|
||||
label="Kategorie"
|
||||
:options="[{'text':'Alle'},{'value':1,'text':'Dienstleistungen'},{'value':2,'text':'Elektromaterial etc.'},{'value':3,'text':'EStmk Shop'},{'value':4,'text':'GPON OLTs und Bridges'},{'value':5,'text':'Kabel-TV und Zubehör'},{'value':6,'text':'Kupferverkabelung und Schränke'},{'value':7,'text':'LWL Aussen- und Universalkabel'},{'value':8,'text':'LWL Boxen, Muffen und Gehäuse'},{'value':9,'text':'LWL Leitungsbau'},{'value':10,'text':'LWL Pigtails und Kupplungen'},{'value':11,'text':'LWL Splitter, Filter und Dämpfer'},{'value':12,'text':'Netzteile, USV, Akkus'},{'value':13,'text':'Patchkabel Kupfer'},{'value':14,'text':'Patchkabel LWL Multimode'},{'value':15,'text':'Patchkabel LWL Singlemode'},{'value':16,'text':'Richtfunk und WLAN'},{'value':17,'text':'Router und Zubehör'},{'value':18,'text':'SFP und Konverter'},{'value':19,'text':'Switches und Zubehör'},{'value':20,'text':'Telefonie und Zubehör'}]"
|
||||
v-model="articleTest"
|
||||
@input="window.console.log('Selected categories:', articleTest)"
|
||||
/>
|
||||
|
||||
<tt-table-crud ref="table" @openHistory="historyModalId = $event.id; historyModal = true">
|
||||
<template v-slot:cheapestsellprice="{ row }">
|
||||
<template v-for="price in JSON.parse(row.cheapestSellPrice || '[]')">
|
||||
@@ -175,7 +187,7 @@ Vue.component('warehouse-article', {
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
</tt-card>
|
||||
`,
|
||||
data: () => ({window, historyModal: false, historyModalId: null}),
|
||||
data: () => ({window, historyModal: false, historyModalId: null, articleTest: null}),
|
||||
mounted() {
|
||||
const table = this.$refs.table?.$refs?.table;
|
||||
if (!table) return;
|
||||
@@ -188,5 +200,9 @@ Vue.component('warehouse-article', {
|
||||
if (Object.keys(table.filters).length === 0) table.filters = {};
|
||||
table.refreshTable();
|
||||
}
|
||||
|
||||
window.addEventListener('refreshTable', () => {
|
||||
table.refreshTable();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,43 +1,208 @@
|
||||
Vue.component('tt-select', {
|
||||
props: {
|
||||
options: {type: Array, required: true},
|
||||
label: {type: String, required: false},
|
||||
required: {type: Boolean, default: false},
|
||||
value: {type: [String, Number], required: false},
|
||||
disabled: {type: Boolean, default: false},
|
||||
suffix: {type: String, required: false},
|
||||
sm: {type: Boolean, default: false},
|
||||
row: {type: Boolean, default: false},
|
||||
options: { type: Array, required: true },
|
||||
label: { type: String, default: null },
|
||||
required: { type: Boolean, default: false },
|
||||
value: { type: [String, Number, Array], default: null },
|
||||
disabled: { type: Boolean, default: false },
|
||||
suffix: { type: String, default: null },
|
||||
sm: { type: Boolean, default: false },
|
||||
row: { type: Boolean, default: false },
|
||||
multiple: { type: Boolean, default: false },
|
||||
searchable: { type: Boolean, default: true }
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedOption: undefined,
|
||||
open: false,
|
||||
searchQuery: '',
|
||||
// internal model: array if multiple, else primitive
|
||||
selected: this.multiple
|
||||
? (Array.isArray(this.value) ? [...this.value] : [])
|
||||
: this.value
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.selectedOption = this.value;
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
this.selectedOption = newValue;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="form-group" :class="{'row': row}">
|
||||
<label v-if="label"
|
||||
:class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}"
|
||||
|
||||
:for="label">{{ label }}</label>
|
||||
<select class="form-control" :class="{'form-control-sm': sm, 'col-sm-8': row}"
|
||||
:required="required" v-model="selectedOption" :disabled="disabled"
|
||||
@change="$emit('input', $event.target.value ? $event.target.value : undefined)">
|
||||
<template v-for="option of options">
|
||||
<option v-if="['string','number'].includes(typeof option)" :value="option" :disabled="option.disabled === true">{{ option }}
|
||||
<template v-if="suffix"> {{ suffix }}</template>
|
||||
</option>
|
||||
<option v-else :value="option.value" :disabled="option.disabled === true">{{ option.text }}</option>
|
||||
</template>
|
||||
</select>
|
||||
watch: {
|
||||
// keep internal model in sync with external v-model
|
||||
value(newVal) {
|
||||
this.selected = this.multiple
|
||||
? (Array.isArray(newVal) ? [...newVal] : [])
|
||||
: newVal;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener('click', this.onClickOutside);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.onClickOutside);
|
||||
},
|
||||
|
||||
computed: {
|
||||
// normalize all options to { value, text, disabled }
|
||||
normalizedOptions() {
|
||||
return this.options.map(opt => {
|
||||
if (opt !== null && typeof opt === 'object') {
|
||||
return {
|
||||
value: opt.value,
|
||||
text: opt.text,
|
||||
disabled: !!opt.disabled
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: opt,
|
||||
text: String(opt),
|
||||
disabled: false
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// filter by searchQuery
|
||||
filteredOptions() {
|
||||
if (!this.searchable || !this.searchQuery) {
|
||||
return this.normalizedOptions;
|
||||
}
|
||||
const q = this.searchQuery.toLowerCase();
|
||||
return this.normalizedOptions.filter(o =>
|
||||
o.text.toLowerCase().includes(q)
|
||||
);
|
||||
},
|
||||
|
||||
// what to show on the button
|
||||
displayText() {
|
||||
if (this.multiple) {
|
||||
const arr = Array.isArray(this.selected) ? this.selected : [];
|
||||
const count = arr.length;
|
||||
if (count === 0) return '';
|
||||
if (count === 1) {
|
||||
const o = this.normalizedOptions.find(x => x.value === arr[0]);
|
||||
return o
|
||||
? o.text + (this.suffix ? ' ' + this.suffix : '')
|
||||
: '';
|
||||
}
|
||||
// more than one selected
|
||||
return `${count} ausgewählt`;
|
||||
}
|
||||
// single select
|
||||
const sel = this.normalizedOptions.find(o => o.value === this.selected);
|
||||
return sel
|
||||
? sel.text + (this.suffix ? ' ' + this.suffix : '')
|
||||
: '';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleDropdown() {
|
||||
if (this.disabled) return;
|
||||
this.open = !this.open;
|
||||
},
|
||||
|
||||
onClickOutside(e) {
|
||||
if (!this.$el.contains(e.target)) {
|
||||
this.open = false;
|
||||
}
|
||||
},
|
||||
|
||||
isSelected(opt) {
|
||||
return this.multiple
|
||||
? Array.isArray(this.selected) && this.selected.includes(opt.value)
|
||||
: this.selected === opt.value;
|
||||
},
|
||||
|
||||
selectOption(opt) {
|
||||
if (opt.disabled) return;
|
||||
if (this.multiple) {
|
||||
const arr = Array.isArray(this.selected) ? [...this.selected] : [];
|
||||
const idx = arr.indexOf(opt.value);
|
||||
if (idx > -1) arr.splice(idx, 1);
|
||||
else arr.push(opt.value);
|
||||
|
||||
this.selected = arr;
|
||||
this.$emit('input', arr); // Fixed: Always emit the array when multiple
|
||||
} else {
|
||||
this.selected = opt.value;
|
||||
this.$emit('input', opt.value);
|
||||
this.open = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
template: `
|
||||
<div class="form-group" :class="{ row: row }">
|
||||
<label v-if="label"
|
||||
:for="label"
|
||||
:class="{
|
||||
'col-form-label': row,
|
||||
'col-sm-4': row,
|
||||
'col-form-label-sm': sm && row
|
||||
}">
|
||||
{{ label }}
|
||||
</label>
|
||||
|
||||
<div :class="row ? 'col-sm-8' : ''">
|
||||
<div class="dropdown" :class="{ show: open }">
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary form-control"
|
||||
:class="{ 'form-control-sm': sm, 'btn-sm': sm }"
|
||||
:disabled="disabled"
|
||||
@click.stop="toggleDropdown"
|
||||
style="display:flex; align-items:center;">
|
||||
|
||||
<span style="
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
">
|
||||
{{ displayText || 'Auswählen...' }}
|
||||
</span>
|
||||
<i class="fas fa-chevron-down ml-2"></i>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-menu p-2"
|
||||
:class="{ show: open }"
|
||||
style="
|
||||
min-width: 100%;
|
||||
max-width: 400px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
">
|
||||
<input v-if="searchable"
|
||||
type="text"
|
||||
class="form-control mb-2"
|
||||
v-model="searchQuery"
|
||||
:disabled="disabled"
|
||||
placeholder="Suchen...">
|
||||
|
||||
<div v-for="opt in filteredOptions"
|
||||
:key="opt.value"
|
||||
class="dropdown-item px-2 py-1"
|
||||
:class="{ disabled: opt.disabled }"
|
||||
@click.stop>
|
||||
<label class="d-flex align-items-center m-0 w-100">
|
||||
<input v-if="multiple"
|
||||
type="checkbox"
|
||||
class="form-check-input mr-2"
|
||||
:checked="isSelected(opt)"
|
||||
:disabled="opt.disabled"
|
||||
@change.prevent="selectOption(opt)">
|
||||
<input v-else
|
||||
type="radio"
|
||||
class="form-check-input mr-2"
|
||||
:name="label"
|
||||
:value="opt.value"
|
||||
:checked="isSelected(opt)"
|
||||
:disabled="opt.disabled"
|
||||
@change.prevent="selectOption(opt)">
|
||||
<span>
|
||||
{{ opt.text }}<span v-if="suffix"> {{ suffix }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
});
|
||||
@@ -162,7 +162,7 @@ Vue.component('tt-table', {
|
||||
<tt-input v-if="column.filter === 'search' && !disableFiltering" v-model="filters[column.key]" sm/>
|
||||
<tt-icon-select v-else-if="column.filter === 'iconSelect' && !disableFiltering" :options="column.filterOptions" v-model="filters[column.key]" sm :multiple="column.filterOptions.length > 2 && this.ssr === true"/>
|
||||
<tt-number-range v-else-if="column.filter === 'numberRange' && !disableFiltering" :returnText="!ssr" v-model="filters[column.key]" sm/>
|
||||
<tt-select v-else-if="column.filter === 'select' && !disableFiltering" :options="[{text: 'Alle', value: undefined}, ...column.filterOptions]" v-model="filters[column.key]" sm/>
|
||||
<tt-select v-else-if="column.filter === 'select' && !disableFiltering" :options="[{text: 'Alle', value: undefined}, ...column.filterOptions]" v-model="filters[column.key]" sm multiple/>
|
||||
<tt-autocomplete v-else-if="column.filter === 'autocomplete' && !disableFiltering" :api-url="column.filterOptions" v-model="filters[column.key]" sm/>
|
||||
<tt-date-picker v-else-if="(column.filter === 'date' || column.filter === 'datepicker') && !disableFiltering" v-model="filters[column.key]" sm/>
|
||||
<!-- @formatter:on -->
|
||||
@@ -562,14 +562,15 @@ Vue.component('tt-table', {
|
||||
filters: {
|
||||
handler: function () {
|
||||
if (!this.isInitialised) return;
|
||||
console.log('Filters changed:', this.filters);
|
||||
|
||||
// go through filters and if there is a set value in filters and the filter of the column is select or autocomplete then parse the value to int
|
||||
for (const key in this.filters) {
|
||||
if (this.filters[key] && (this.columns[key].filter === 'select' || this.columns[key].filter === 'autocomplete')) {
|
||||
// only if first character is a number
|
||||
if (!isNaN(this.filters[key][0])) {
|
||||
this.filters[key] = parseInt(this.filters[key]);
|
||||
}
|
||||
// if (!isNaN(this.filters[key][0])) {
|
||||
// this.filters[key] = parseInt(this.filters[key]);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user