Updated Warehouse

This commit is contained in:
Luca Haid
2025-02-04 19:06:08 +01:00
parent 514d5d5d7e
commit 7c8af7aed4
24 changed files with 1126 additions and 99 deletions
@@ -0,0 +1,14 @@
@media (min-width: 992px) {
.modal-lg, .modal-xl {
/*max width either 90% or 1120px*/
max-width: min(90vw, 1120px) !important;
}
}
@media (max-width: 992px) {
.warehouse-order-modal-positions-entry-container {
display: grid;
grid-template-columns: 1fr 1fr !important;
grid-gap: 10px;
}
}
@@ -0,0 +1,178 @@
Vue.component('warehouse-offer-modal', {
props: {
id: {type: [String, Number], required: true},
mode: {type: String, default: 'edit'}
},
template: `
<tt-modal :show="true"
@submit="submit"
:delete="id !== 'create'"
:title="id === 'create' ? 'Angebot erstellen' : \`Angebot #\${id} bearbeiten\`"
@update:show="$emit('close')">
<div style="width: 99%"><h4 class="text-center">Angebotdetails</h4>
<tt-select label="Sachbearbeiter"
:options="window.TT_CONFIG.CRUD_CONFIG.columns.find(column => column.key === 'createBy')?.modal.items"
sm
row
v-model="offer.editor"/>
<tt-input label="Kundennummer" v-model="offer.customerNumber" sm row/>
<tt-input label="Kundenreferenz" v-model="offer.reference" sm row/>
<tt-textarea label="Angebotszweck" v-model="offer.purpose" sm row/>
<hr>
<h4 class="text-center">Kundenadresse</h4>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 2fr 2fr 1fr 1fr 2fr;">
<tt-input label="Name" v-model="offer.customerName" sm/>
<tt-input label="Straße" v-model="offer.customerStreet" sm/>
<tt-input label="PLZ" v-model="offer.customerZip" sm/>
<tt-input label="Ort" v-model="offer.customerCity" sm/>
<tt-input label="UID" v-model="offer.customerVAT" sm/>
</div>
<hr>
<h4 class="text-center">Positionen</h4>
<tt-positions-manager ref="positionsManager" v-model="offer.positions" :config="positionsConfig" @updateField-article="fetchArticleData"/>
<hr>
<h4 class="text-center">Alternative Artikel</h4>
<tt-positions-manager ref="alternativePositionsManager" v-model="offer.alternativePositions" :config="alternativePositionsConfig"/>
<hr>
<tt-input label="Gesamtrabatt (%)" v-model="offer.totalDiscount" sm row type="number"/>
<tt-select label="Zahlungskonditionen" :options="paymentTerms" sm row v-model="offer.paymentTerms"/>
<tt-select label="Lieferkonditionen" :options="deliveryTerms" sm row v-model="offer.deliveryTerms"/>
<tt-select label="Schlusstext" :options="closingTexts" sm row v-model="offer.closingText"/>
<hr>
<tt-textarea label="Notizen" v-model="offer.notes" sm row/>
</div>
</tt-modal>
`,
data() {
return {
window: window,
positionsConfig: {
fields: {
article: {
type: 'autocomplete',
label: 'Artikel',
apiUrl: '/WarehouseArticle/autoComplete',
customFieldReference: 'WarehouseArticle',
},
amount: {type: 'input', label: 'Menge', inputType: 'number'},
unit: {type: 'input', label: 'Einheit'},
articleNumber: {type: 'input', label: 'Artikelnummer'},
unitPrice: {type: 'input', label: 'Einzelpreis', inputType: 'number'},
discount: {type: 'input', label: 'Rabatt (%)', inputType: 'number'},
},
validateForm: (formData) => {
const requiredFields = ['article', 'amount', 'unitPrice'];
for (const field of requiredFields) {
if (!formData[field]) {
window.notify('error', `Bitte füllen Sie ${this.positionsConfig.fields[field].label} aus`);
return false;
}
}
return true;
},
},
alternativePositionsConfig: {
fields: {
article: {type: 'input', label: 'Artikel'},
description: {type: 'textarea', label: 'Beschreibung'},
},
},
paymentTerms: [
{value: 'net30', text: '30 Tage netto'},
{value: 'net60', text: '60 Tage netto'},
{value: 'immediate', text: 'Sofort fällig'},
],
deliveryTerms: [
{value: 'ex_works', text: 'Ab Werk'},
{value: 'free_delivery', text: 'Frei Haus'},
{value: 'fob', text: 'FOB'},
],
closingTexts: [
{value: 'standard', text: 'Standardtext'},
{value: 'custom1', text: 'Angepasster Text 1'},
{value: 'custom2', text: 'Angepasster Text 2'},
],
offer: {
editor: window.TT_CONFIG['USER_ID'],
customerNumber: '',
reference: '',
purpose: '',
customerName: '',
customerStreet: '',
customerZip: '',
customerCity: '',
customerVAT: '',
positions: [],
alternativePositions: [],
totalDiscount: 0,
paymentTerms: 'net30',
deliveryTerms: 'ex_works',
closingText: 'standard',
notes: '',
}
}
},
async mounted() {
if (this.id !== 'create') {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/getById`, {params: {id: this.id}});
this.offer = response.data;
this.offer.positions = JSON.parse(this.offer.positions);
this.offer.alternativePositions = JSON.parse(this.offer.alternativePositions);
}
},
methods: {
async submit() {
if (this.offer.positions.length === 0) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
const url = this.id === 'create'
? `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/create`
: `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOffer/update`;
const response = await axios.post(url, this.offer);
if (response.data.success) {
window.notify('success', response.data.message ?? 'Angebot erfolgreich gespeichert');
this.$emit('close');
} else {
window.notify('error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
}
},
async fetchArticleData(article) {
if (typeof article === 'number') {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticle/getById`, {params: {id: article}});
this.$refs.positionsManager.updateField('articleNumber', response.data.articleNumber);
this.$refs.positionsManager.updateField('unitPrice',
Object.values(JSON.parse(response.data.cheapestSellPrice)).find(price => price.title === 'Verkauf').price);
this.$refs.positionsManager.updateField('unit', response.data.unit);
}
},
},
});
Vue.component('warehouse-offer', {
template: `
<tt-card>
<warehouse-offer-modal v-if="offerModalId" :id="offerModalId" @close="offerModalId = null;$refs.table.$refs.table.refreshTable()"/>
<button @click="offerModalId = 'create'" class="btn btn-primary">Angebot erstellen</button>
<tt-table-crud emit-edit @edit="offerModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }">
<div>
<h5>Notizen</h5>
<p>{{ row.notes }}</p>
<h5>Verlauf</h5>
<ul>
<li v-for="entry in row.journal">{{ entry.date }} - {{ entry.description }}</li>
</ul>
</div>
</template>
</tt-table-crud>
</tt-card>
`,
data() {
return {
window: window,
offerModalId: null,
}
},
});
@@ -24,4 +24,29 @@
grid-template-columns: 1fr 1fr !important;
grid-gap: 10px;
}
}
}
/* Expanded Row Styling */
.order-summary {
padding: 1rem;
}
.position-item {
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.position-header {
background-color: #f0f0f0;
padding: 0.5rem;
font-weight: bold;
}
.position-details {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
padding: 0.5rem;
}
.field-item {
margin-bottom: 0.5rem;
}
+101 -24
View File
@@ -16,6 +16,7 @@ Vue.component('warehouse-order-modal', {
sm
row
v-model="order.editor"/>
<tt-input label="Externe Referenz" v-model="order.extReference" sm row/>
<hr>
<h4 class="text-center">Positionen</h4>
@@ -80,6 +81,7 @@ Vue.component('warehouse-order-modal', {
},
},
order: {
extReference: '',
delAddrName: 'XINON GmbH',
delAddrLine: 'Fladnitz im Raabtal 150',
delAddrPLZ: '8322',
@@ -92,11 +94,13 @@ Vue.component('warehouse-order-modal', {
}
},
async mounted() {
if (this.id !== 'create') {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.id}});
response.data.positions = JSON.parse(response.data.positions);
this.order = response.data;
}
if (this.id === 'create') return;
console.log(this.id);
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById?disableParse`, {params: {id: this.id}});
response.data.positions = JSON.parse(response.data.positions);
this.order = response.data;
},
methods: {
async submit() {
@@ -112,18 +116,20 @@ Vue.component('warehouse-order-modal', {
positions: this.order.positions.filter(position => position.distributorId === distributorId)
}
);
if (response.data.success) window.notify('success', response.data.message ?? 'Bestellung erfolgreich erstellt');
else window.notify('error',
if (response.data.success) {
this.$emit('close');
window.notify('success', response.data.message ?? 'Bestellung erfolgreich erstellt');
} else window.notify('error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
}
} else {
const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/update`, this.order);
if (response.data.success) window.notify('success', response.data.message ?? 'Bestellung erfolgreich aktualisiert');
else window.notify('error',
if (response.data.success) {
this.$emit('close');
window.notify('success', response.data.message ?? 'Bestellung erfolgreich aktualisiert');
} else window.notify('error',
response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
}
this.$emit('close');
},
async fetchDistributors(article) {
const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getArticleDistributorData`;
@@ -148,22 +154,93 @@ Vue.component('warehouse-order-modal', {
},
});
Vue.component('warehouse-order', {
Vue.component('warehouse-order-detail', {
//language=Vue
template: `
<tt-card>
<warehouse-order-modal v-if="orderModalId" :id="orderModalId" @close="orderModalId = null;$refs.table.$refs.table.refreshTable()"/>
<button @click="orderModalId = 'create'" class="btn btn-primary">Bestellung erstellen</button>
<tt-table-crud emit-edit @edit="orderModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }"><span>Work in Progress</span></template>
</tt-table-crud>
</tt-card>
`, data() {
return {
window: window,
orderModalId: null,
<template v-slot:header><h4>Bestellungsdetails für #{{ loading ? 'Laden...' : order.orderNumber }}</h4></template>
<template v-if="loading">
<div 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>
</template>
<template v-else>
<h3>Lieferadresse</h3>
<div>{{order.delAddrName}}</div>
<div>{{order.delAddrEMail}}</div>
<div>{{order.delAddrLine}}</div>
<div>{{order.delAddrPLZ}} {{order.delAddrCity}}</div>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;margin-top: 24px">
<div><strong>Artikel</strong></div>
<div><strong>Menge</strong></div>
<div><strong>Preis</strong></div>
<div><strong>Lieferant</strong></div>
<div><strong>Verwendung</strong></div>
<div><strong>Summe</strong></div>
</div>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;" v-for="position in order.positions">
<div>{{ position.articleName }}</div>
<div>{{ position.amount }}</div>
<div>{{ position.buyPrice }}</div>
<div>{{ position.distributorName }}</div>
<div>{{ position.verwendung }}</div>
<div>{{ position.amount * position.buyPrice }}</div>
</div>
</template>
</tt-card>
`,
props: {
id: {type: [String, Number], required: true}
},
data() {
return {
window: window,
order: {},
loading: true
}
},
})
async mounted() {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getById`, {params: {id: this.id}});
this.order = response.data;
this.loading = false;
},
});
Vue.component('warehouse-order', {
template: `
<tt-card>
<warehouse-order-modal v-if="orderModalId" :id="orderModalId" @close="closeOrderModal"/>
<button @click="orderModalId = 'create'" class="btn btn-primary">Bestellung erstellen</button>
<tt-table-crud emit-edit @edit="orderModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }">
<warehouse-order-detail :id="row['id']"/>
</template>
<template v-slot:sum="{ row }">{{ calculateSum(JSON.parse(row["positions"])).toFixed(2)}} €</template>
<!-- TODO: think of a way here prob we add it to the database as field-->
<template v-slot:distributor="{ row }">{{ row.id % 2 == 0 ? 'Triotronik' : 'Discomp' }}</template>
</tt-table-crud>
</tt-card>
`,
data() {
return {
orderModalId: null,
}
},
methods: {
closeOrderModal() {
this.orderModalId = null;
this.$refs.table.$refs.table.refreshTable();
},
calculateSum(positions) {
return positions.reduce((sum, position) => sum + position.amount * position.buyPrice, 0);
}
}
});
@@ -0,0 +1,14 @@
@media (min-width: 992px) {
.modal-lg, .modal-xl {
/*max width either 90% or 1120px*/
max-width: min(90vw, 1120px) !important;
}
}
@media (max-width: 992px) {
.warehouse-order-modal-positions-entry-container {
display: grid;
grid-template-columns: 1fr 1fr !important;
grid-gap: 10px;
}
}
@@ -0,0 +1,161 @@
Vue.component('warehouse-project-modal', {
props: {
id: { type: [String, Number], required: true },
mode: { type: String, default: 'edit' }
},
template: `
<tt-modal :show="true"
@submit="submit"
:delete="id !== 'create'"
:title="id === 'create' ? 'Projekt erstellen' : \`Projekt #\${id} bearbeiten\`"
@update:show="$emit('close')">
<div style="width: 99%">
<h4 class="text-center">Projektübersicht</h4>
<tt-input label="Projektnummer" v-model="project.projectNumber" sm row disabled />
<tt-textarea label="Um was handelt es sich?" v-model="project.description" sm row/>
<hr>
<h4 class="text-center">Zeitraum</h4>
<div style="display: grid; grid-gap: 10px; grid-template-columns: 1fr 1fr;">
<tt-date-picker label="Startdatum" v-model="project.startDate" sm/>
<tt-date-picker label="Enddatum" v-model="project.endDate" sm/>
</div>
<hr>
<h4 class="text-center">Beteiligte Personen</h4>
<tt-select label="Personen (XINON MT)"
:options="participantsOptions"
v-model="project.participants"
sm row />
<tt-textarea label="Freitext für weitere Personen" v-model="project.additionalParticipants" sm row/>
<hr>
<h4 class="text-center">Projektübersicht</h4>
<tt-input label="Gesamtsumme des Projekts (€)" v-model.number="project.totalSum" sm row type="number"/>
<tt-positions-manager
ref="positionsManager"
v-model="project.positions"
:config="positionsConfig"
@updateField-article="fetchArticleData"
/>
<hr>
<h4 class="text-center">Lagerort</h4>
<tt-input label="Lagerort für dieses Projekt" v-model="project.storageLocation" sm row/>
<hr>
<tt-textarea label="Notizen" v-model="project.notes" sm row/>
</div>
</tt-modal>
`,
data() {
return {
window: window,
participantsOptions: [
{ value: 1, text: 'Person A' },
{ value: 2, text: 'Person B' },
{ value: 3, text: 'Person C' }
// Add more participants as needed
],
positionsConfig: {
fields: {
article: {
type: 'autocomplete',
label: 'Artikel',
apiUrl: '/WarehouseArticle/autoComplete',
customFieldReference: 'WarehouseArticle',
},
hoursRequired: { type: 'input', label: 'Benötigte Stunden', inputType: 'number' },
amountRequired: { type: 'input', label: 'Benötigte Menge', inputType: 'number' },
description: { type: 'textarea', label: 'Beschreibung' }
},
validateForm(formData) {
const requiredFields = ['article', 'hoursRequired', 'amountRequired'];
for (const field of requiredFields) {
if (!formData[field]) {
window.notify('error', `Bitte füllen Sie ${this.positionsConfig.fields[field].label} aus`);
return false;
}
}
return true;
}
},
project: {
projectNumber: '',
description: '',
startDate: null,
endDate: null,
participants: [],
additionalParticipants: '',
totalSum: 0,
positions: [],
storageLocation: '',
notes: ''
}
};
},
async mounted() {
if (this.id !== 'create') {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/getById`, { params: { id: this.id } });
this.project = response.data;
} else {
this.project.projectNumber = await this.generateProjectNumber();
}
},
methods: {
async submit() {
if (!this.project.description) return window.notify('error', 'Bitte geben Sie eine Beschreibung ein.');
const url = this.id === 'create'
? `${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/create`
: `${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/update`;
const response = await axios.post(url, this.project);
if (response.data.success) {
window.notify('success', response.data.message ?? 'Projekt erfolgreich gespeichert');
this.$emit('close');
} else {
window.notify('error', response.data.errors ? Object.values(response.data.errors).join('<br>') : response.data.message || 'Ein Fehler ist aufgetreten');
}
},
async fetchArticleData(article) {
if (typeof article === 'number') {
const response = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseArticle/getById`, { params: { id: article } });
this.$refs.positionsManager.updateField('description', response.data.description);
}
},
async generateProjectNumber() {
const currentCount = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseProject/count`);
return `PRJ-${new Date().getFullYear()}-${String(currentCount.data + 1).padStart(4, '0')}`;
}
}
});
Vue.component('warehouse-project', {
template: `
<tt-card>
<warehouse-project-modal v-if="projectModalId" :id="projectModalId" @close="projectModalId = null;$refs.table.$refs.table.refreshTable()"/>
<button @click="projectModalId = 'create'" class="btn btn-primary">Angebot erstellen</button>
<tt-table-crud emit-edit @edit="projectModalId = $event.id" ref="table">
<template v-slot:expandedRow="{ row }">
<div>
<h5>Notizen</h5>
<p>{{ row.notes }}</p>
<h5>Verlauf</h5>
<ul>
<li v-for="entry in row.journal">{{ entry.date }} - {{ entry.description }}</li>
</ul>
</div>
</template>
</tt-table-crud>
</tt-card>
`,
data() {
return {
window: window,
projectModalId: null,
}
},
});