Merge branch 'WarehouseArticle/improve-modal' into 'master'
Warehouse article/improve modal See merge request fronk/thetool!1937
This commit is contained in:
@@ -241,12 +241,13 @@ class Termination extends mfBaseModel {
|
||||
$this->log->debug("is range");
|
||||
$port_parts = explode("-", $ports);
|
||||
if(is_array($port_parts) && count($port_parts) == 2) {
|
||||
$from = $port_parts[0];
|
||||
$to = $port_parts[1];
|
||||
$from = intval($port_parts[0]);
|
||||
$to = intval($port_parts[1]);
|
||||
|
||||
if($port_parts[0] > $port_parts[1]) {
|
||||
$from = $port_parts[1];
|
||||
$to = $port_parts[0];
|
||||
if($from > $to) {
|
||||
$tmp = $from;
|
||||
$from = $to;
|
||||
$to = $tmp;
|
||||
}
|
||||
|
||||
$range = [];
|
||||
|
||||
@@ -1,43 +1,464 @@
|
||||
/* End of Life Row Highlighting */
|
||||
.end-of-life {
|
||||
background-color: #f8d7da !important;
|
||||
}
|
||||
|
||||
.warehouse-article-prices > div,
|
||||
.warehouse-article-distributor > div {
|
||||
/*
|
||||
* Modal Layout
|
||||
*/
|
||||
.modal-body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.wa-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.wa-modal-content .row {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.wa-modal-content .row > [class*="col-"] {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.wa-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.wa-section-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/*
|
||||
* Article Prices Section - Dense 2-Column Layout
|
||||
*/
|
||||
.wa-prices-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.wa-prices-grid-dense {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr)) 120px;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.wa-price-item {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
transition: box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.wa-price-item:hover {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.wa-price-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.warehouse-article-prices .form-group,
|
||||
.warehouse-article-distributor .form-group {
|
||||
.wa-price-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wa-price-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.wa-price-actions .btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.wa-price-actions .btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/*
|
||||
* Section Title with Action Button
|
||||
*/
|
||||
.wa-section-title-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
margin: -12px -12px 12px -12px;
|
||||
background: linear-gradient(to bottom, #ffffff, #f8f9fa);
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Distributor Section - With Modal Directory
|
||||
*/
|
||||
.wa-distributors-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.wa-directory-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wa-directory-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.wa-directory-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.wa-directory-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.warehouse-article-prices > div,
|
||||
.warehouse-article-distributor > div {
|
||||
display: block;
|
||||
border: 1px solid #eee;
|
||||
padding: 10px;
|
||||
.wa-directory-letter {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: #007bff;
|
||||
padding: 4px 8px;
|
||||
background: #e7f3ff;
|
||||
border-left: 3px solid #007bff;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wa-directory-items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 6px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.wa-directory-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
background: white;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.wa-directory-item:hover:not(:disabled) {
|
||||
background: #007bff;
|
||||
border-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.wa-directory-item.active {
|
||||
background: #28a745;
|
||||
border-color: #28a745;
|
||||
color: white;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.wa-directory-item:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wa-distributors-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.wa-distributor-row {
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
transition: box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.wa-distributor-row:hover {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.wa-distributor-name {
|
||||
font-size: 0.8rem;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wa-distributor-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wa-distributor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.wa-distributor-actions .btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.wa-distributor-actions .btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/*
|
||||
* Custom Checkboxes - Similar to WorkorderMphBase
|
||||
*/
|
||||
.wa-checkbox-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.wa-checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wa-checkbox-item:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
.wa-checkbox-item input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wa-checkmark {
|
||||
position: relative;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
min-width: 16px;
|
||||
background-color: #fff;
|
||||
border: 2px solid #adb5bd;
|
||||
border-radius: 3px;
|
||||
margin-right: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.wa-checkbox-item input[type="checkbox"]:checked ~ .wa-checkmark {
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.wa-checkmark:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 4px;
|
||||
top: 0px;
|
||||
width: 4px;
|
||||
height: 8px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.wa-checkbox-item input[type="checkbox"]:checked ~ .wa-checkmark:after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wa-checkbox-label {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/*
|
||||
* Form Controls - Dense Sizing (Desktop Only)
|
||||
*/
|
||||
@media (min-width: 768px) {
|
||||
.wa-modal-content .form-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.warehouse-article-prices > div > *,
|
||||
.warehouse-article-distributor > div > * {
|
||||
display: block;
|
||||
margin-bottom: 10px !important;
|
||||
.wa-modal-content label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.warehouse-article-prices > div > div:last-child,
|
||||
.warehouse-article-distributor > div > div:last-child {
|
||||
text-align: right;
|
||||
margin-bottom: 0 !important;
|
||||
.wa-modal-content .form-control-sm,
|
||||
.wa-modal-content .tt-select-trigger.sm {
|
||||
height: 30px;
|
||||
font-size: 0.8rem;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.warehouse-article-prices > div > div:last-child > *,
|
||||
.warehouse-article-distributor > div > div:last-child > * {
|
||||
margin-left: 5px;
|
||||
/* Ensure tt-select aligns with tt-input */
|
||||
.wa-modal-content .tt-select-modern {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.wa-modal-content .tt-select-modern .form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wa-modal-content .tt-select-modern label {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.wa-modal-content textarea.form-control {
|
||||
min-height: 60px;
|
||||
font-size: 0.8rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Additional Attributes Section
|
||||
*/
|
||||
.wa-attributes-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.wa-attributes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Responsive Design
|
||||
*/
|
||||
@media (max-width: 992px) {
|
||||
.wa-prices-grid-dense {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wa-directory-items {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wa-checkbox-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.wa-modal-content .row > [class*="col-"] {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.wa-price-body {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wa-price-actions {
|
||||
justify-content: flex-end;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.wa-distributor-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wa-distributor-name {
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.wa-distributor-inputs {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wa-distributor-actions {
|
||||
justify-content: flex-end;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.wa-checkbox-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.wa-section-title {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.wa-modal-content {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.wa-section {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.wa-section-title-with-action {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,29 +16,55 @@ async function handleApiResponse(responsePromise) {
|
||||
Vue.component('warehouse-article-prices', {
|
||||
props: {id: {type: Number, required: true}},
|
||||
template: `
|
||||
<tt-card>
|
||||
<h4 style="text-align: center">Artikelpreise überschreiben</h4>
|
||||
<div class="warehouse-article-prices">
|
||||
<div v-for="(price, typeTitle) in articlePrices" :key="typeTitle">
|
||||
<div style="align-self: center;">
|
||||
<i v-if="price.isRobot" class="fa-solid fa-robot"></i> {{ typeTitle }}
|
||||
<div class="wa-prices-section">
|
||||
<h5 class="wa-section-title"><i class="fas fa-tags mr-2"></i>Artikelpreise überschreiben</h5>
|
||||
<div class="wa-prices-grid-dense">
|
||||
<div v-for="(price, typeTitle) in articlePrices" :key="typeTitle" class="wa-price-item">
|
||||
<div class="wa-price-header">
|
||||
<i v-if="price.isRobot" class="fas fa-robot mr-1 text-muted" style="font-size: 0.75rem;"></i>
|
||||
<strong style="font-size: 0.8rem;">{{ typeTitle }}</strong>
|
||||
<i v-if="price.pendingChanges" class="fas fa-exclamation-triangle text-warning ml-auto"
|
||||
style="font-size: 0.75rem;" title="Nicht gespeichert"></i>
|
||||
</div>
|
||||
<tt-input sm v-model="price.priceMultiplier" label="Preisfaktor" @input="price.priceOverride = null;price.pendingChanges = true"/>
|
||||
<tt-input sm v-model="price.priceOverride" label="Preis" @input="price.priceMultiplier = null;price.pendingChanges = true"/>
|
||||
<div style="align-self: end;display: flex;align-items: center;">
|
||||
<tt-button sm icon="fa-solid fa-save" additional-class="btn-primary" @click="savePrice(price)"/>
|
||||
<tt-button sm icon="fa-solid fa-trash" additional-class="btn-danger" v-if="!price.isRobot" @click="deletePrice(price)"/>
|
||||
<i v-if="price.pendingChanges" class="fa-solid fa-triangle-exclamation ml-1 text-warning" style="font-size: 28px" title="Dieser Preis wurde noch nicht gespeichert"></i>
|
||||
<div class="wa-price-body">
|
||||
<tt-input
|
||||
v-model="price.priceMultiplier"
|
||||
@input="handleFactorInput(price)"
|
||||
placeholder="Faktor"
|
||||
type="number"
|
||||
sm no-form-group/>
|
||||
<tt-input
|
||||
v-model="price.priceOverride"
|
||||
@input="handlePriceInput(price)"
|
||||
placeholder="Preis €"
|
||||
type="number"
|
||||
sm no-form-group/>
|
||||
<div class="wa-price-actions">
|
||||
<button class="btn btn-sm btn-primary" @click="savePrice(price)" title="Speichern">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" @click="deletePrice(price)" :disabled="price.isRobot" title="Löschen">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</tt-card>
|
||||
</div>
|
||||
`,
|
||||
data: () => ({window, articlePrices: {}}),
|
||||
async mounted() {
|
||||
await this.fetchArticlePrices();
|
||||
},
|
||||
methods: {
|
||||
handleFactorInput(price) {
|
||||
if (price.priceMultiplier) price.priceOverride = null;
|
||||
price.pendingChanges = true;
|
||||
},
|
||||
handlePriceInput(price) {
|
||||
if (price.priceOverride) price.priceMultiplier = null;
|
||||
price.pendingChanges = true;
|
||||
},
|
||||
async fetchArticlePrices() {
|
||||
const [pricesRes, typesRes] = await Promise.all([
|
||||
axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/get`, {filters: {articleId: this.id}}),
|
||||
@@ -53,7 +79,8 @@ Vue.component('warehouse-article-prices', {
|
||||
});
|
||||
pricesRes.data.rows.forEach(pData => {
|
||||
const type = typesRes.data.rows.find(t => t.id === pData.articlePriceTypeId);
|
||||
if (type) prices[type.title] = {
|
||||
if (!type) return;
|
||||
prices[type.title] = {
|
||||
id: pData.id,
|
||||
isRobot: false,
|
||||
pendingChanges: false,
|
||||
@@ -71,13 +98,10 @@ Vue.component('warehouse-article-prices', {
|
||||
priceMultiplier: price.priceMultiplier ? parseFloat(price.priceMultiplier.toString().replace(',', '.')) : null,
|
||||
priceOverride: price.priceOverride ? parseFloat(price.priceOverride.toString().replace(',', '.')) : null
|
||||
};
|
||||
if (price.isRobot) {
|
||||
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/create`, payload));
|
||||
await this.fetchArticlePrices();
|
||||
} else {
|
||||
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/update`, {id: price.id, ...payload}));
|
||||
await this.fetchArticlePrices();
|
||||
}
|
||||
const endpoint = price.isRobot ? 'create' : 'update';
|
||||
const data = price.isRobot ? payload : {id: price.id, ...payload};
|
||||
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticlePrice/${endpoint}`, data));
|
||||
await this.fetchArticlePrices();
|
||||
},
|
||||
async deletePrice(price) {
|
||||
const payload = {id: price.id, articleId: this.id, articlePriceTypeId: price.articlePriceTypeId}
|
||||
@@ -87,42 +111,187 @@ Vue.component('warehouse-article-prices', {
|
||||
}
|
||||
});
|
||||
|
||||
// warehouse-article-distributor.vue.js
|
||||
Vue.component('warehouse-article-distributor', {
|
||||
props: {id: {type: Number, required: true}},
|
||||
Vue.component('warehouse-distributor-directory-modal', {
|
||||
props: {
|
||||
show: { type: Boolean, default: false },
|
||||
allDistributors: { type: Array, default: () => [] },
|
||||
articleDistributors: { type: Array, default: () => [] }
|
||||
},
|
||||
data: () => ({
|
||||
distributorSearch: ''
|
||||
}),
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (newVal) document.documentElement.style.overflow = 'hidden';
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredDistributors() {
|
||||
if (!this.distributorSearch) return this.allDistributors;
|
||||
const search = this.distributorSearch.toLowerCase();
|
||||
return this.allDistributors.filter(d => d.name.toLowerCase().includes(search));
|
||||
},
|
||||
alphabetWithDistributors() {
|
||||
const letters = new Set();
|
||||
this.filteredDistributors.forEach(d => {
|
||||
const firstChar = d.name.charAt(0).toUpperCase();
|
||||
if (/[A-Z]/.test(firstChar)) letters.add(firstChar);
|
||||
});
|
||||
return Array.from(letters).sort();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getDistributorsByLetter(letter) {
|
||||
return this.filteredDistributors.filter(d => d.name.charAt(0).toUpperCase() === letter);
|
||||
},
|
||||
isDistributorAdded(distributorId) {
|
||||
return this.articleDistributors.some(d => d.distributorId === distributorId);
|
||||
},
|
||||
selectDistributor(distributorId) {
|
||||
this.$emit('select', distributorId);
|
||||
this.$emit('close');
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<tt-card>
|
||||
<h4 style="text-align: center">Lieferanten für diesen Artikel</h4>
|
||||
<tt-autocomplete :api-url="window['TT_CONFIG']['BASE_PATH'] + '/WarehouseDistributor/autocomplete'"
|
||||
v-model="newDistributorId" label="Neuen Lieferant hinzufügen"/>
|
||||
<div class="warehouse-article-distributor">
|
||||
<div v-for="(distributor, index) in articleDistributors" :key="distributor.id || ('new-' + index)">
|
||||
<tt-resolver style="align-self: center;" reference="WarehouseDistributor"
|
||||
:value="distributor.distributorId"></tt-resolver>
|
||||
<tt-input sm v-model="distributor.externalArticleNumber" label="Externe Artikelnummer" @input="distributor.pendingChanges = true"/>
|
||||
<tt-input sm v-model="distributor.purchasePrice" label="Einkaufspreis" @input="distributor.pendingChanges = true"/>
|
||||
<div style="align-self: end; display: flex;align-items: center;">
|
||||
<tt-button sm icon="fa-solid fa-save" additional-class="btn-primary"
|
||||
@click="saveDistributor(distributor)"/>
|
||||
<tt-button sm icon="fa-solid fa-trash" additional-class="btn-danger" v-if="distributor.id"
|
||||
@click="deleteDistributor(distributor.id)"/>
|
||||
<i v-if="distributor.pendingChanges || !distributor.id" class="fa-solid fa-triangle-exclamation ml-1 text-warning"
|
||||
style="font-size: 28px" title="Dieser Preis wurde noch nicht gespeichert"></i>
|
||||
|
||||
<div v-if="show" class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);" @click.self="close">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-truck mr-2"></i>
|
||||
Lieferant hinzufügen
|
||||
</h5>
|
||||
<button type="button" class="close" @click="close">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="wa-directory-header mb-3">
|
||||
<tt-input
|
||||
v-model="distributorSearch"
|
||||
placeholder="Lieferant suchen..."
|
||||
sm
|
||||
prefix-icon="fas fa-search"/>
|
||||
</div>
|
||||
<div class="wa-directory-list">
|
||||
<div v-for="letter in alphabetWithDistributors" :key="letter" class="wa-directory-group">
|
||||
<div class="wa-directory-letter">{{ letter }}</div>
|
||||
<div class="wa-directory-items">
|
||||
<button v-for="dist in getDistributorsByLetter(letter)"
|
||||
:key="dist.id"
|
||||
class="wa-directory-item"
|
||||
:class="{ 'active': isDistributorAdded(dist.id) }"
|
||||
@click="selectDistributor(dist.id)"
|
||||
:disabled="isDistributorAdded(dist.id)">
|
||||
{{ dist.name }}
|
||||
<i v-if="isDistributorAdded(dist.id)" class="fas fa-check ml-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</tt-card>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
Vue.component('warehouse-article-distributor', {
|
||||
props: {id: {type: Number, required: true}},
|
||||
template: `
|
||||
<div class="wa-distributors-section">
|
||||
<div class="wa-section-title-with-action">
|
||||
<h5 class="wa-section-title mb-0"><i class="fas fa-truck mr-2"></i>Lieferanten</h5>
|
||||
<button class="btn btn-sm btn-primary" @click="showDirectoryModal = true">
|
||||
<i class="fas fa-plus mr-1"></i>Lieferant hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<warehouse-distributor-directory-modal
|
||||
:show="showDirectoryModal"
|
||||
:all-distributors="allDistributors"
|
||||
:article-distributors="articleDistributors"
|
||||
@select="addDistributor"
|
||||
@close="showDirectoryModal = false"/>
|
||||
|
||||
<div v-if="articleDistributors.length === 0" class="text-muted text-center py-3" style="font-size: 0.85rem;">
|
||||
<i class="fas fa-info-circle mr-1"></i>Keine Lieferanten zugewiesen
|
||||
</div>
|
||||
|
||||
<div v-else class="wa-distributors-grid">
|
||||
<div v-for="(distributor, index) in articleDistributors" :key="distributor.id || ('new-' + index)" class="wa-distributor-row">
|
||||
<div class="wa-distributor-name">
|
||||
<strong>{{ getDistributorName(distributor.distributorId) }}</strong>
|
||||
<i v-if="distributor.pendingChanges || !distributor.id"
|
||||
class="fas fa-exclamation-triangle text-warning ml-1"
|
||||
style="font-size: 0.75rem;"
|
||||
title="Nicht gespeichert"></i>
|
||||
</div>
|
||||
<div class="wa-distributor-inputs">
|
||||
<tt-input
|
||||
v-model="distributor.externalArticleNumber"
|
||||
@input="distributor.pendingChanges = true"
|
||||
placeholder="Externe Art.-Nr."
|
||||
sm no-form-group/>
|
||||
<tt-input
|
||||
v-model="distributor.purchasePrice"
|
||||
@input="distributor.pendingChanges = true"
|
||||
placeholder="Einkaufspreis €"
|
||||
sm no-form-group/>
|
||||
</div>
|
||||
<div class="wa-distributor-actions">
|
||||
<button class="btn btn-sm btn-primary" @click="saveDistributor(distributor)" title="Speichern">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" @click="deleteDistributor(distributor.id)" :disabled="!distributor.id" title="Löschen">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data: () => ({window, articleDistributors: [], newDistributorId: null}),
|
||||
data: () => ({
|
||||
window,
|
||||
articleDistributors: [],
|
||||
allDistributors: [],
|
||||
showDirectoryModal: false
|
||||
}),
|
||||
async mounted() {
|
||||
await this.fetchArticleDistributors();
|
||||
await Promise.all([
|
||||
this.fetchArticleDistributors(),
|
||||
this.fetchAllDistributors()
|
||||
]);
|
||||
},
|
||||
methods: {
|
||||
async fetchAllDistributors() {
|
||||
const res = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseDistributor/get`, {
|
||||
pagination: false,
|
||||
order: { key: 'name', order: 'ASC' }
|
||||
});
|
||||
this.allDistributors = res.data.rows || [];
|
||||
},
|
||||
async fetchArticleDistributors() {
|
||||
const res = await axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/get`, {filters: {articleId: this.id}});
|
||||
this.articleDistributors = res.data.rows;
|
||||
},
|
||||
getDistributorName(id) {
|
||||
const dist = this.allDistributors.find(d => d.id === id);
|
||||
return dist ? dist.name : 'Unbekannt';
|
||||
},
|
||||
addDistributor(distributorId) {
|
||||
if (this.articleDistributors.some(d => d.distributorId === distributorId)) return;
|
||||
this.articleDistributors.push({
|
||||
articleId: this.id,
|
||||
distributorId: distributorId,
|
||||
externalArticleNumber: null,
|
||||
purchasePrice: null,
|
||||
pendingChanges: true
|
||||
});
|
||||
},
|
||||
async saveDistributor(distributor) {
|
||||
delete distributor.pendingChanges;
|
||||
distributor.purchasePrice = distributor.purchasePrice ? parseFloat(distributor.purchasePrice.toString().replace(',', '.')) : null;
|
||||
@@ -130,34 +299,330 @@ Vue.component('warehouse-article-distributor', {
|
||||
await this.fetchArticleDistributors();
|
||||
},
|
||||
async deleteDistributor(distributorId) {
|
||||
if (!confirm('Lieferant wirklich entfernen?')) return;
|
||||
await this.window.handleApiResponse(axios.post(`${window['TT_CONFIG']['BASE_PATH']}/WarehouseArticleDistributor/delete`, {id: distributorId, articleId: this.id}));
|
||||
await this.fetchArticleDistributors();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
newDistributorId(newId) {
|
||||
if (newId) {
|
||||
if (!this.articleDistributors.some(d => d.distributorId === newId)) {
|
||||
this.articleDistributors.push({
|
||||
articleId: this.id,
|
||||
distributorId: newId,
|
||||
externalArticleNumber: null,
|
||||
purchasePrice: null
|
||||
});
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.newDistributorId = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// warehouse-article.vue.js
|
||||
Vue.component('warehouse-article-modal', {
|
||||
props: {
|
||||
id: { type: [Number, String], required: true }
|
||||
},
|
||||
template: `
|
||||
<div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);" @click.self="close">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-box mr-2"></i>
|
||||
{{ isEditMode ? 'Artikel bearbeiten' : 'Artikel erstellen' }}
|
||||
</h5>
|
||||
<button type="button" class="close" @click="close">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div v-if="loading" class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
</div>
|
||||
<div v-else class="wa-modal-content">
|
||||
<!-- Basic Information -->
|
||||
<div class="wa-section">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<tt-input
|
||||
label="Titel"
|
||||
v-model="formData.title"
|
||||
placeholder="Artikel Titel"
|
||||
required
|
||||
sm/>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<tt-textarea
|
||||
label="Beschreibung"
|
||||
v-model="formData.description"
|
||||
required
|
||||
rows="3"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<tt-select
|
||||
label="Kategorie"
|
||||
v-model="formData.category_id"
|
||||
:options="categoryOptions"
|
||||
required
|
||||
sm/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<tt-input
|
||||
label="Artikel-Nummer"
|
||||
v-model="formData.articleNumber"
|
||||
placeholder="z.B. 1234"
|
||||
required
|
||||
form-label
|
||||
sm/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<tt-select
|
||||
label="Einheit"
|
||||
v-model="formData.unit"
|
||||
:options="unitOptions"
|
||||
required
|
||||
sm/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<tt-select
|
||||
label="Erlöskonto"
|
||||
v-model="formData.revenueAccount"
|
||||
:options="revenueAccountOptions"
|
||||
required
|
||||
sm/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prices Section -->
|
||||
<warehouse-article-prices v-if="isEditMode" :id="Number(id)"/>
|
||||
|
||||
<!-- Distributors Section -->
|
||||
<warehouse-article-distributor v-if="isEditMode" :id="Number(id)"/>
|
||||
|
||||
<!-- Additional Attributes -->
|
||||
<div class="wa-section">
|
||||
<h5 class="wa-section-title"><i class="fas fa-cog mr-2"></i>Zusätzliche Artikel Attribute</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<tt-input
|
||||
label="Warnmenge"
|
||||
type="number"
|
||||
v-model.number="formData.warningAmount"
|
||||
placeholder="0"
|
||||
required
|
||||
sm/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<tt-input
|
||||
label="Kritische Menge"
|
||||
type="number"
|
||||
v-model.number="formData.criticalAmount"
|
||||
placeholder="0"
|
||||
required
|
||||
sm/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wa-checkbox-grid">
|
||||
<label class="wa-checkbox-item">
|
||||
<input type="checkbox" v-model="formData.isSerialDocumentation">
|
||||
<span class="wa-checkmark"></span>
|
||||
<span class="wa-checkbox-label">Seriennummern</span>
|
||||
</label>
|
||||
<label class="wa-checkbox-item">
|
||||
<input type="checkbox" v-model="formData.isEndOfLife">
|
||||
<span class="wa-checkmark"></span>
|
||||
<span class="wa-checkbox-label">End of Life</span>
|
||||
</label>
|
||||
<label class="wa-checkbox-item">
|
||||
<input type="checkbox" v-model="formData.isEShop">
|
||||
<span class="wa-checkmark"></span>
|
||||
<span class="wa-checkbox-label">Ist E-Shop</span>
|
||||
</label>
|
||||
<label class="wa-checkbox-item">
|
||||
<input type="checkbox" v-model="formData.isEShopHide">
|
||||
<span class="wa-checkmark"></span>
|
||||
<span class="wa-checkbox-label">E-Shop Versteckt</span>
|
||||
</label>
|
||||
<label class="wa-checkbox-item">
|
||||
<input type="checkbox" v-model="formData.isSbidiShop">
|
||||
<span class="wa-checkmark"></span>
|
||||
<span class="wa-checkbox-label">Ist SBIDI-Shop</span>
|
||||
</label>
|
||||
<label class="wa-checkbox-item">
|
||||
<input type="checkbox" v-model="formData.isSbidiShopHide">
|
||||
<span class="wa-checkmark"></span>
|
||||
<span class="wa-checkbox-label">SBIDI-Shop Versteckt</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="close">Abbrechen</button>
|
||||
<button type="button" class="btn btn-primary" @click="save" :disabled="saving || !isValid">
|
||||
<i v-if="saving" class="fas fa-spinner fa-spin mr-1"></i>
|
||||
<i v-else class="fas fa-save mr-1"></i>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data: () => ({
|
||||
loading: false,
|
||||
saving: false,
|
||||
formData: {
|
||||
title: '',
|
||||
description: '',
|
||||
category_id: null,
|
||||
articleNumber: '',
|
||||
unit: 'Stk.',
|
||||
revenueAccount: 0,
|
||||
warningAmount: 0,
|
||||
criticalAmount: 0,
|
||||
isSerialDocumentation: false,
|
||||
isEndOfLife: false,
|
||||
isEShop: false,
|
||||
isEShopHide: false,
|
||||
isSbidiShop: false,
|
||||
isSbidiShopHide: false
|
||||
}
|
||||
}),
|
||||
computed: {
|
||||
isEditMode() {
|
||||
return this.id !== 'create';
|
||||
},
|
||||
categoryOptions() {
|
||||
const catCol = window.TT_CONFIG.CRUD_CONFIG.columns.find(c => c.key === 'category_id');
|
||||
return catCol ? [{ value: null, text: '-- Bitte wählen --' }, ...catCol.modal.items] : [];
|
||||
},
|
||||
unitOptions() {
|
||||
return [
|
||||
{ value: 'Stk.', text: 'Stk.' },
|
||||
{ value: 'Pau.', text: 'Pau.' },
|
||||
{ value: 'm.', text: 'm.' },
|
||||
{ value: 'Std.', text: 'Std.' },
|
||||
{ value: 'km', text: 'km' }
|
||||
];
|
||||
},
|
||||
revenueAccountOptions() {
|
||||
return [
|
||||
{ value: 0, text: 'Dienstleistungen' },
|
||||
{ value: 1, text: 'Handelswaren' }
|
||||
];
|
||||
},
|
||||
isValid() {
|
||||
return this.formData.title &&
|
||||
this.formData.description &&
|
||||
this.formData.category_id &&
|
||||
this.formData.articleNumber &&
|
||||
this.formData.unit;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
document.documentElement.style.paddingRight = scrollbarWidth + 'px';
|
||||
|
||||
if (this.isEditMode) this.loadArticle();
|
||||
else this.resetForm();
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.documentElement.style.overflow = '';
|
||||
document.documentElement.style.paddingRight = '';
|
||||
},
|
||||
methods: {
|
||||
async loadArticle() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseArticle/getById`, {
|
||||
params: { id: Number(this.id) }
|
||||
});
|
||||
const data = res.data;
|
||||
if (data && data.id) {
|
||||
this.formData = {
|
||||
title: data.title || '',
|
||||
description: data.description || '',
|
||||
category_id: data.category_id,
|
||||
articleNumber: data.articleNumber || '',
|
||||
unit: data.unit || 'Stk.',
|
||||
revenueAccount: data.revenueAccount || 0,
|
||||
warningAmount: data.warningAmount || 0,
|
||||
criticalAmount: data.criticalAmount || 0,
|
||||
isSerialDocumentation: !!data.isSerialDocumentation,
|
||||
isEndOfLife: !!data.isEndOfLife,
|
||||
isEShop: !!data.isEShop,
|
||||
isEShopHide: !!data.isEShopHide,
|
||||
isSbidiShop: !!data.isSbidiShop,
|
||||
isSbidiShopHide: !!data.isSbidiShopHide
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
window.notify('error', 'Fehler beim Laden');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.formData = {
|
||||
title: '',
|
||||
description: '',
|
||||
category_id: null,
|
||||
articleNumber: '',
|
||||
unit: 'Stk.',
|
||||
revenueAccount: 0,
|
||||
warningAmount: 0,
|
||||
criticalAmount: 0,
|
||||
isSerialDocumentation: false,
|
||||
isEndOfLife: false,
|
||||
isEShop: false,
|
||||
isEShopHide: false,
|
||||
isSbidiShop: false,
|
||||
isSbidiShopHide: false
|
||||
};
|
||||
},
|
||||
async save() {
|
||||
if (!this.isValid) return;
|
||||
this.saving = true;
|
||||
try {
|
||||
const endpoint = this.isEditMode ? 'update' : 'create';
|
||||
const payload = {
|
||||
...this.formData,
|
||||
isSerialDocumentation: this.formData.isSerialDocumentation ? 1 : 0,
|
||||
isEndOfLife: this.formData.isEndOfLife ? 1 : 0,
|
||||
isEShop: this.formData.isEShop ? 1 : 0,
|
||||
isEShopHide: this.formData.isEShopHide ? 1 : 0,
|
||||
isSbidiShop: this.formData.isSbidiShop ? 1 : 0,
|
||||
isSbidiShopHide: this.formData.isSbidiShopHide ? 1 : 0
|
||||
};
|
||||
if (this.isEditMode) payload.id = Number(this.id);
|
||||
|
||||
const res = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WarehouseArticle/${endpoint}`, payload);
|
||||
if (res.data.success) {
|
||||
window.notify('success', res.data.message || 'Gespeichert');
|
||||
if (!this.isEditMode && window.TT_CONFIG.CRUD_CONFIG.reopenOnCreate) this.$emit('reopen', res.data.id);
|
||||
else this.$emit('close');
|
||||
} else {
|
||||
window.notify('error', res.data.message || 'Fehler beim Speichern');
|
||||
}
|
||||
} catch (e) {
|
||||
window.notify('error', 'Fehler beim Speichern');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('warehouse-article', {
|
||||
template: `
|
||||
<tt-card>
|
||||
<tt-table-crud ref="table" @openHistory="historyModalId = $event.id; historyModal = true">
|
||||
<warehouse-article-modal
|
||||
v-if="articleModalId"
|
||||
:id="articleModalId"
|
||||
@close="articleModalId = null; $refs.table.$refs.table.refreshTable()"
|
||||
@reopen="articleModalId = $event"/>
|
||||
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
|
||||
<tt-table-crud
|
||||
ref="table"
|
||||
emit-edit
|
||||
@openHistory="historyModalId = $event.id; historyModal = true"
|
||||
@edit="articleModalId = $event.id">
|
||||
<template v-slot:cheapestsellprice="{ row }">
|
||||
<template v-for="price in JSON.parse(row.cheapestSellPrice || '[]')">
|
||||
<span style="white-space:nowrap;" v-if="price && window.TT_CONFIG['WAREHOUSE_ADMIN']">{{ price.title }}: <span
|
||||
@@ -165,7 +630,7 @@ Vue.component('warehouse-article', {
|
||||
<span v-else-if="price && price.title === 'Verkauf'">{{ price.price }} €</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
<template v-slot:description="{ row }">
|
||||
<tt-tooltip
|
||||
allow-wrapping
|
||||
@@ -175,16 +640,15 @@ Vue.component('warehouse-article', {
|
||||
</tt-tooltip>
|
||||
<span v-else>{{ row.description }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:modal-prepend="{ crudModalData }">
|
||||
<warehouse-article-prices v-if="crudModalData.id" :id="crudModalData.id"/>
|
||||
<warehouse-article-distributor v-if="crudModalData.id" :id="crudModalData.id"/>
|
||||
</template>
|
||||
</tt-table-crud>
|
||||
<warehouse-history-modal :show.sync="historyModal" :id="historyModalId"/>
|
||||
</tt-card>
|
||||
`,
|
||||
data: () => ({window, historyModal: false, historyModalId: null, articleTest: null}),
|
||||
data: () => ({
|
||||
window,
|
||||
historyModal: false,
|
||||
historyModalId: null,
|
||||
articleModalId: null
|
||||
}),
|
||||
mounted() {
|
||||
const table = this.$refs.table?.$refs?.table;
|
||||
if (!table) return;
|
||||
@@ -197,9 +661,5 @@ Vue.component('warehouse-article', {
|
||||
if (Object.keys(table.filters).length === 0) table.filters = {};
|
||||
table.refreshTable();
|
||||
}
|
||||
|
||||
window.addEventListener('refreshTable', () => {
|
||||
table.refreshTable();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ Vue.component('tt-input', {
|
||||
placeholder: String,
|
||||
required: Boolean,
|
||||
row: Boolean,
|
||||
formLabel: Boolean,
|
||||
value: [String, Number],
|
||||
hint: String,
|
||||
additionalProps: Object,
|
||||
@@ -27,7 +28,7 @@ Vue.component('tt-input', {
|
||||
<div :class="{'row': row, 'form-group' : !noFormGroup, 'tt-input-with-icon': prefixIcon}">
|
||||
<slot name="prepend"></slot>
|
||||
<label
|
||||
:class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}"
|
||||
:class="{'col-form-label': row || formLabel, 'col-sm-4': row, 'col-form-label-sm': sm && row}"
|
||||
v-if="label"
|
||||
:for="label">{{ label }}</label>
|
||||
<i v-if="prefixIcon" :class="['prefix-icon', prefixIcon]"></i>
|
||||
|
||||
@@ -1,90 +1,89 @@
|
||||
Vue.component('tt-modal', {
|
||||
props: {
|
||||
show: {type: [Boolean, Object], default: false},
|
||||
title: {type: String, default: 'Überschrift'},
|
||||
delete: {type: Boolean, default: true},
|
||||
deleteText: {type: String, default: 'Löschen'},
|
||||
save: {type: Boolean, default: true},
|
||||
props: {
|
||||
show: {type: [Boolean, Object], default: false},
|
||||
title: {type: String, default: 'Überschrift'},
|
||||
delete: {type: Boolean, default: true},
|
||||
deleteText: {type: String, default: 'Löschen'},
|
||||
save: {type: Boolean, default: true},
|
||||
saveLoading: {type: Boolean, default: false},
|
||||
saveText: {type: String, default: 'Speichern'},
|
||||
disableMinHeight: {type: Boolean, default: false},
|
||||
}, watch: {
|
||||
saveText: {type: String, default: 'Speichern'},
|
||||
disableMinHeight: {type: Boolean, default: false}
|
||||
},
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('close')
|
||||
document.documentElement.style.overflow = ''
|
||||
document.documentElement.style.paddingRight = ''
|
||||
}
|
||||
if (newVal) {
|
||||
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
|
||||
document.documentElement.style.overflow = 'hidden'
|
||||
document.documentElement.style.paddingRight = scrollbarWidth + 'px'
|
||||
this.$nextTick(() => {
|
||||
const input = this.$refs.modal.querySelector('input')
|
||||
if (input) {
|
||||
if (input.classList.contains('tt-autocomplete')) {
|
||||
return
|
||||
}
|
||||
input.focus()
|
||||
}
|
||||
if (input && !input.classList.contains('tt-autocomplete')) input.focus()
|
||||
})
|
||||
}
|
||||
},
|
||||
}, // create global listener for esc + return key (if save is enabled)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
document.addEventListener('keydown', this.keydownHandler)
|
||||
}, destroyed() {
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('keydown', this.keydownHandler)
|
||||
}, methods: {
|
||||
document.documentElement.style.overflow = ''
|
||||
document.documentElement.style.paddingRight = ''
|
||||
},
|
||||
methods: {
|
||||
keydownHandler(event) {
|
||||
if (!this.show) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
this.$emit('update:show', false)
|
||||
}
|
||||
if (!this.show) return
|
||||
if (event.key === 'Escape') this.$emit('update:show', false)
|
||||
if (event.key === 'Enter' && this.save) {
|
||||
if (event.target.tagName === 'TEXTAREA' || event.target.tagName === 'INPUT') {
|
||||
return
|
||||
}
|
||||
if (event.target.tagName === 'TEXTAREA' || event.target.tagName === 'INPUT') return
|
||||
this.$emit('submit')
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {window: window, isMobile: navigator.userAgent.match(/(iPad)|(iPhone)|(iPod)|(android)|(webOS)/i) !== null}
|
||||
},
|
||||
//language=Vue
|
||||
data: () => ({
|
||||
window: window,
|
||||
isMobile: navigator.userAgent.match(/(iPad)|(iPhone)|(iPod)|(android)|(webOS)/i) !== null
|
||||
}),
|
||||
template: `
|
||||
<div class="modal show d-block"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
style="background: rgba(0, 0, 0, 0.5);"
|
||||
ref="modal"
|
||||
@keydown.esc="$emit('update:show', false)"
|
||||
v-if="show">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document" @mousedown.stop>
|
||||
<div class="modal-content" :style="{minHeight: disableMinHeight ? 'auto' : '45vh'}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{title}}</h5>
|
||||
<button type="button" class="close" @click="$emit('update:show', false)">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<slot name="footer">
|
||||
<slot name="footer-prepend"></slot>
|
||||
<button v-if="save" class="btn btn-primary position-relative" @click="$emit('submit')" :disabled="saveLoading">
|
||||
<span v-if="!saveLoading">{{ saveText }}</span>
|
||||
<span v-if="saveLoading" class="top-50 start-50 translate-middle">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</span>
|
||||
</button>
|
||||
<button v-if="$props.delete" class="btn btn-danger" @click="$emit('delete')">{{deleteText}}</button>
|
||||
<button class="btn btn-secondary" @click="$emit('update:show', false)">Schließen</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
<div class="modal show d-block"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
style="background: rgba(0, 0, 0, 0.5);"
|
||||
ref="modal"
|
||||
@keydown.esc="$emit('update:show', false)"
|
||||
v-if="show">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document" @mousedown.stop>
|
||||
<div class="modal-content" :style="{minHeight: disableMinHeight ? 'auto' : '45vh'}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{title}}</h5>
|
||||
<button type="button" class="close" @click="$emit('update:show', false)">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<slot name="footer">
|
||||
<slot name="footer-prepend"></slot>
|
||||
<button v-if="save" class="btn btn-primary position-relative" @click="$emit('submit')" :disabled="saveLoading">
|
||||
<span v-if="!saveLoading">{{ saveText }}</span>
|
||||
<span v-if="saveLoading" class="top-50 start-50 translate-middle">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</span>
|
||||
</button>
|
||||
<button v-if="$props.delete" class="btn btn-danger" @click="$emit('delete')">{{deleteText}}</button>
|
||||
<button class="btn btn-secondary" @click="$emit('update:show', false)">Schließen</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user