feat: combine multiple orderrequests into one order

This commit is contained in:
Luca Haid
2025-04-08 10:06:52 +02:00
parent 4797e2bcb8
commit 7f33125389
9 changed files with 186 additions and 34 deletions

View File

@@ -21,6 +21,7 @@ $additionalCSS = [
...$additionalCSS,
'plugins/daterangepicker/daterangepicker.css',
'plugins/vue/tt-components/css/tt-table.css',
'plugins/vue/tt-components/css/tt-tooltip.css',
'plugins/vue/tt-components/css/tt-loader.css',
'plugins/vue/tt-components/css/tt-position-manager.css',
];

View File

@@ -16,7 +16,7 @@ class WarehouseShippingNoteController extends TTCrud {
['value' => 'cancelled', 'text' => 'Storniert', 'icon' => 'fas fa-ban text-danger'],
['value' => 'on_hold', 'text' => 'In Wartestellung', 'icon' => 'fas fa-pause text-warning'],
]]],
['key' => 'type', 'text' => 'Typ', 'required' => true],
['key' => 'type', 'text' => 'Typ', 'required' => false],
['key' => 'deliveryAddressName', 'text' => 'L.-Adr. Name', 'required' => true],
['key' => 'deliveryAddressLine', 'text' => 'L.-Adr.', 'required' => true],
['key' => 'deliveryAddressPLZ', 'text' => 'L.-Adr. PLZ', 'required' => true],

View File

@@ -48,6 +48,7 @@ $jsFiles = [
"plugins/vue/tt-components/tt-checkbox.js",
"plugins/vue/tt-components/tt-textarea.js",
"plugins/vue/tt-components/tt-position-manager.js",
"plugins/vue/tt-components/tt-tooltip.js",
];

View File

@@ -373,23 +373,26 @@ Vue.component('warehouse-order-modal', {
return;
}
const orderRequest = JSON.parse(localStorage.getItem('WarehouseOrder_create'));
if (!orderRequest) return;
const orderRequests = JSON.parse(localStorage.getItem('WarehouseOrder_create'));
if (!orderRequests) return;
const positions = JSON.parse(orderRequest.positions);
this.order.positions = await Promise.all(positions.map(async p => {
const distributor = (await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getArticleDistributorData`,
{params: {articleId: p.articleId}})).data[0];
return {
article: p.articleId,
amount: p.amount,
buyPrice: distributor.purchasePrice,
distributorId: distributor.id,
distributorArticleNumber: distributor.externalArticleNumber,
verwendung: `${p.hasOwnProperty('purpose') ? p.purpose : ''} [Bestellwunsch: #${orderRequest.id}]`,
linkedOrderRequestId: orderRequest.id
};
}));
for (const orderRequest of orderRequests) {
const positions = JSON.parse(orderRequest.positions);
const parsedPositions = await Promise.all(positions.map(async p => {
const distributor = (await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrder/getArticleDistributorData`,
{params: {articleId: p.articleId}})).data[0];
return {
article: p.articleId,
amount: p.amount,
buyPrice: distributor.purchasePrice,
distributorId: distributor.id,
distributorArticleNumber: distributor.externalArticleNumber,
verwendung: `${p.hasOwnProperty('purpose') ? p.purpose : ''} [Bestellwunsch: #${orderRequest.id}]`,
linkedOrderRequestId: orderRequest.id
};
}));
this.order.positions = [...this.order.positions, ...parsedPositions];
}
localStorage.removeItem('WarehouseOrder_create');
},

View File

@@ -17,8 +17,8 @@ window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [
condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1' && row.cancelled === 1,
},
{
key: "createOrder",
title: "Bestellung erstellen",
key: "addOrderToCart",
title: "Bestellung in den Warenkorb",
class: "fas fa-shopping-cart text-primary", // Use shopping-cart to indicate order creation
condition: (row) => window.TT_CONFIG['WAREHOUSE_ADMIN'] === '1'
&& row.cancelled === 0 && (!row.linkedOrderIds || row.linkedOrderIds.length === 0)
@@ -203,7 +203,18 @@ Vue.component('warehouse-order-request', {
@undoneOrder="undoneOrder"
@createLog="createLog"
@createOrder="createOrder"
@addOrderToCart="addOrderToCart"
ref="crud">
<template #table-top-buttons>
<tt-tooltip :text="createOrdersButtonToolTipText">
<tt-button
:disabled="orderShoppingCart.length === 0"
@click="createOrder"
additional-class="btn-outline-success text-center"
:text="createOrdersButtonText" icon="fas fa-shopping-cart"/>
</tt-tooltip>
</template>
<template #linkedorderids="{row}">
<linked-order-status :linkedOrders="row.linkedOrderIds" v-if="row.linkedOrderIds"/>
</template>
@@ -229,7 +240,8 @@ Vue.component('warehouse-order-request', {
addLogModalId: null,
showHiddenRequests: false,
showCanceledRequests: false,
orderRequestModalId: null
orderRequestModalId: null,
orderShoppingCart: [],
}),
methods: {
openHistory(e) {
@@ -263,12 +275,26 @@ Vue.component('warehouse-order-request', {
uncancelRequest(row) {
this.cancelRequest(row, '0');
},
async createOrder(row) {
const res = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WarehouseOrderRequest/getById?id=${row.id}`);
async createOrder() {
if (this.orderShoppingCart.length > 0) {
localStorage.setItem('WarehouseOrder_create', JSON.stringify(this.orderShoppingCart));
window.location.href = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder`;
} else window.notify('warning', 'Warenkorb ist leer');
},
async addOrderToCart(row) {
const res = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrderRequest/getById?id=${row.id}`);
if (res.data?.positions && typeof res.data.positions === 'string') {
localStorage.setItem('WarehouseOrder_create', JSON.stringify(res.data));
window.location.href = `${window.TT_CONFIG.BASE_PATH}/WarehouseOrder`;
} else window.notify('error', res.data.message || 'Fehler beim erstellen der Bestellung');
this.orderShoppingCart.push(res.data);
window.notify('success', 'Bestellung in den Warenkorb gelegt');
} else window.notify('error', res.data.message || 'Fehler beim hinzufügen der Bestellung');
}
},
computed: {
createOrdersButtonText: function () {
return this.orderShoppingCart.length > 0 ? `${this.orderShoppingCart.length} Bestellung(en) erstellen` : 'Bestellung(en) erstellen';
},
createOrdersButtonToolTipText: function () {
return this.orderShoppingCart.length > 0 ? `Erstellt ${this.orderShoppingCart.length} Bestellung(en)` : 'Keine Bestellung im Warenkorb';
}
}
});

View File

@@ -0,0 +1,96 @@
.tt-tooltip-wrapper {
position: relative;
display: inline-block; /* Or 'block' depending on your layout needs */
cursor: pointer;
}
.tt-tooltip-box {
position: absolute;
background-color: #333; /* Dark background */
color: #fff; /* White text */
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 10; /* Ensure it's above other elements */
opacity: 0; /* Hidden by default, fade in */
transition: opacity 0.3s;
pointer-events: none; /* Prevent tooltip from interfering with mouse events */
text-align: center; /* Center text */
}
/* Make tooltip visible when showTooltip is true */
.tt-tooltip-wrapper:hover .tt-tooltip-box {
opacity: 1;
}
/* Positioning */
.tt-tooltip-top {
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 5px; /* Space between element and tooltip */
}
.tt-tooltip-bottom {
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 5px;
}
.tt-tooltip-left {
right: 100%;
top: 50%;
transform: translateY(-50%);
margin-right: 5px;
}
.tt-tooltip-right {
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: 5px;
}
/* Optional: Add arrows */
.tt-tooltip-box::after {
content: '';
position: absolute;
border-width: 5px;
border-style: solid;
}
.tt-tooltip-top::after {
top: 100%;
left: 50%;
margin-left: -5px;
border-color: #333 transparent transparent transparent;
}
.tt-tooltip-bottom::after {
bottom: 100%;
left: 50%;
margin-left: -5px;
border-color: transparent transparent #333 transparent;
}
.tt-tooltip-left::after {
top: 50%;
left: 100%;
margin-top: -5px;
border-color: transparent transparent transparent #333;
}
.tt-tooltip-right::after {
top: 50%;
right: 100%;
margin-top: -5px;
border-color: transparent #333 transparent transparent;
}
.tt-tooltip-wrapper > * {
display: inline-block; /* Ensure the tooltip wrapper behaves correctly */
width: 100% !important;
}

View File

@@ -2,9 +2,7 @@
Vue.component('tt-button', {
//language=Vue
template: `
<div>
<template v-if="href">
<a :href="href" class="btn" :class="buttonClasses" @click="handleClick">
<a v-if="href" :href="href" class="btn" :class="buttonClasses" @click="handleClick">
<template v-if="loading">
<span class="spinner"></span>
</template>
@@ -13,9 +11,7 @@ Vue.component('tt-button', {
{{text}}
</template>
</a>
</template>
<template v-else>
<button @click="handleClick" class="btn" :class="buttonClasses" :disabled="loading">
<button v-else @click="handleClick" class="btn" :class="buttonClasses" :disabled="loading">
<template v-if="loading">
<span class="spinner"></span>
</template>
@@ -24,8 +20,6 @@ Vue.component('tt-button', {
{{text}}
</template>
</button>
</template>
</div>
`,
props: {
sm: {type: Boolean, default: false},

View File

@@ -65,7 +65,7 @@ Vue.component('tt-table-crud', {
<tt-input v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'number'" v-model="crudModalData[column.key]" :label="column.text" type="number" sm row/>
<tt-textarea v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'textarea'" v-model="crudModalData[column.key]" :label="column.text" sm row/>
<tt-select v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'select'" v-model="crudModalData[column.key]" :label="column.text" :options="column.items" sm row/>
<tt-autocomplete v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'autocomplete'" v-model="crudModalData[column.key]" :label="column.text" :api-url="column.apiUrl" :items="column.items" sm row :return-text="column.returnText" />
<tt-autocomplete v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'autocomplete'" v-model="crudModalData[column.key]" :label="column.text" :api-url="column.apiUrl" :items="typeof column.items === 'string' ? [] : column.items" sm row :return-text="column.returnText" />
<tt-date-picker v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'datepicker'" v-model="crudModalData[column.key]" :label="column.text" sm row :date-range="false" :ref="column.key.toLowerCase() + '-modal-input'"/>
<tt-icon-select v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'icon-select'" v-model="crudModalData[column.key]" :options="column.items" :label="column.text" sm row/>
<tt-checkbox v-show="crudModalColumnVisibility[column.key]" v-else-if="column.type === 'checkbox'" v-model="crudModalData[column.key]" :label="column.text" sm row/>

View File

@@ -0,0 +1,31 @@
Vue.component('tt-tooltip', {
props: {
text: {
type: String,
required: true,
default: 'Tooltip text'
},
position: {
type: String,
default: 'top', // Options: top, bottom, left, right
validator: function (value) {
// The value must match one of these strings
return ['top', 'bottom', 'left', 'right'].indexOf(value) !== -1;
}
}
},
data() {
return {
showTooltip: true
};
},
template: `
<div class="tt-tooltip-wrapper"
@mouseenter="showTooltip = true"
@mouseleave="showTooltip = false">
<slot></slot> <div v-if="showTooltip" class="tt-tooltip-box" :class="['tt-tooltip-' + position]">
{{ text }}
</div>
</div>
`
});