= $StdHeader ?>
@@ -289,7 +289,7 @@ foreach ($devicesall as $deviceall) {
| Pop Name |
- $devices->pop->id]) ?>">= $devices->pop->name ?>
+ = ($me->is('Admin')) ? ' $devices->pop->id]) . '">' . $devices->pop->name . '' : $devices->pop->name ?>
|
@@ -338,67 +338,75 @@ foreach ($devicesall as $deviceall) {
}
?>
-
-
-
Config Backups
- devicetype->devicemanufactor->config_backup > count()): ?>
-
-
-
-
- success == "true" && $devicesconfig->data > count()) {
- ?>
-
-
-
-
- | Datum/Uhrzeit |
- |
-
-
-
- data as $config) :
- $configfileCleartext = trim($config->config_cleartext);
- $configfileCompressed = trim($config->config_compressed);
- $configid = $config->id;
- if ($configfileCleartext && $configfileCompressed) :
+ is('Admin')) : ?>
+
+
+
Config Backups
+ devicetype->devicemanufactor->config_backup > count()): ?>
+
+ is('Admin')):
+ ?>
+
+
+
+ success == "true" && $devicesconfig->data > count()) {
+ ?>
+
+
+
+
+ | Datum/Uhrzeit |
+ |
+
+
+
+ data as $config) :
+ $configfileCleartext = trim($config->config_cleartext);
+ $configfileCompressed = trim($config->config_compressed);
+ $configid = $config->id;
+ if ($configfileCleartext && $configfileCompressed) :
- $configLinks = ' 'getconfig', 'id' => $configid, 'format' => 'txt', 'filename' => $configfileCleartext]) . '">
+ $configLinks = ' 'getconfig', 'id' => $configid, 'format' => 'txt', 'filename' => $configfileCleartext]) . '">
TXT / 'getconfig', 'id' => $configid, 'format' => 'xml', 'filename' => $configfileCompressed]) . '">
XML';
- elseif ($configfileCleartext || $configfileCompressed) :
- $configLinks = ' 'getconfig', 'id' => $configid, 'format' => 'txt', 'filename' => $configfileCleartext . $configfileCompressed]) . '">
+ elseif ($configfileCleartext || $configfileCompressed) :
+ $configLinks = ' 'getconfig', 'id' => $configid, 'format' => 'txt', 'filename' => $configfileCleartext . $configfileCompressed]) . '">
TXT';
- endif;
- ?>
-
- | = date("d.m.Y/H:i", $config->config_timestamp); ?> |
- = $configLinks; ?> |
+ endif;
+ ?>
+
+ | = date("d.m.Y/H:i", $config->config_timestamp); ?> |
+ = $configLinks; ?> |
-
-
+
+
-
-
-
-
+
+
+
+
+
Keine Configs vorhanden
+
+
-
-
Keine Configs vorhanden
-
-
-
+
+
+
devicetype->olt && TT_MBI_API_ENABLE) :
@@ -647,6 +655,7 @@ foreach ($devicesall as $deviceall) {
$config = str_replace("&&YEAR&&", $year, $config);
$config = str_replace("&&MONTH&&", $month, $config);
$config = str_replace("&&DAY&&", $day, $config);
+ $config = str_replace("&&IP&&", $devices->ip, $config);
?>
diff --git a/Layout/default/Invoice/Index.php b/Layout/default/Invoice/Index.php
index 9e22ad4cb..5f269cc04 100644
--- a/Layout/default/Invoice/Index.php
+++ b/Layout/default/Invoice/Index.php
@@ -55,7 +55,7 @@ $pagination_entity_name = "Rechnungen";
"/>
-
+
"/>
@@ -474,4 +474,4 @@ $pagination_entity_name = "Rechnungen";
-
\ No newline at end of file
+
diff --git a/Layout/default/Preorder/Index.php b/Layout/default/Preorder/Index.php
index 01ce07859..1e4677ea4 100644
--- a/Layout/default/Preorder/Index.php
+++ b/Layout/default/Preorder/Index.php
@@ -738,7 +738,9 @@ $pagination_entity_name = "Vorbestellungen";
var marker_popup_content = '';
// popup fields
+ const preorder_view_url = `=self::getUrl("Preorder")?>/Index?filter[ucode]=${preorder.ucode}#preorder=${preorder.id}`;
[
+ ["PREORDER_URL", preorder_view_url],
["street", preorder.adb_strasse],
["hausnummer", preorder.adb_hausnummer],
["zip", preorder.adb_plz],
diff --git a/Layout/default/Preorder/include/preorder_popup.php b/Layout/default/Preorder/include/preorder_popup.php
index e32a66709..9c580d8ab 100644
--- a/Layout/default/Preorder/include/preorder_popup.php
+++ b/Layout/default/Preorder/include/preorder_popup.php
@@ -37,6 +37,11 @@ ob_start();
Email: |
{{EMAIL}} |
-
+
+
+
+ Bestellung ansehen
+
+
=str_replace("\n"," ",ob_get_clean())?>
\ No newline at end of file
diff --git a/Layout/default/TimerecordingEmployee/Form.php b/Layout/default/TimerecordingEmployee/Form.php
index 58e7b1d76..15c8cf7f5 100644
--- a/Layout/default/TimerecordingEmployee/Form.php
+++ b/Layout/default/TimerecordingEmployee/Form.php
@@ -4,6 +4,7 @@ foreach ($days as $key => $day) {
$daysSelect .= '
';
}
$daysSelect .= "";
+
?>
-
\ No newline at end of file
+
diff --git a/Layout/default/VueViews/Vue.php b/Layout/default/VueViews/Vue.php
index 491c0e354..35a73afdd 100644
--- a/Layout/default/VueViews/Vue.php
+++ b/Layout/default/VueViews/Vue.php
@@ -1,64 +1,47 @@
-
:path="window['TT_CONFIG']['PATH']">
-
- <>>
+ <>
+>
-
\ No newline at end of file
+
diff --git a/Layout/default/WarehouseOrder/PDF_FOOTER.html b/Layout/default/WarehouseOrder/PDF_FOOTER.html
new file mode 100644
index 000000000..0a984b7d6
--- /dev/null
+++ b/Layout/default/WarehouseOrder/PDF_FOOTER.html
@@ -0,0 +1,43 @@
+
+
+
+
Xinon Rechnung
+
+
+
+
+
+
+
+
+ XINON GmbH | Fladnitz 150 | 8322 Studenzen
+ Tel.: +43 3115 40800 | E-Mail: office@xinon.at
+ UID: ATU68711968 | FN: 416556h | LG: Feldbach
+ IBAN: {{ bank_iban }} | BIC: {{ bank_bic }}
+
+
+
Seite von
+
+
+
+
diff --git a/Layout/default/WarehouseOrder/PDF_HEADER.html b/Layout/default/WarehouseOrder/PDF_HEADER.html
new file mode 100644
index 000000000..21802f9d5
--- /dev/null
+++ b/Layout/default/WarehouseOrder/PDF_HEADER.html
@@ -0,0 +1,92 @@
+
+
+
+
XINON Shipping Note Header
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+ Lieferant
+ {{ addressLine_1 }}
+ {{ addressLine_2 }}
+ {{ addressLine_3 }}
+ {{ addressLine_4 }}
+
+ {{ externalReference }}
+ |
+
+ Rechnungsadresse
+ {{ billingAddressLine_1 }}
+ {{ billingAddressLine_2 }}
+ {{ billingAddressLine_3 }}
+ {{ billingAddressLine_4 }}
+ {{ billingAddressLine_5 }}
+ {{ billingAddressLine_6 }}
+ |
+
+ {{ shippingAddressLine_1 }}
+ {{ shippingAddressLine_2 }}
+ {{ shippingAddressLine_3 }}
+ {{ shippingAddressLine_4 }}
+ |
+
+
+
+
+
+
+
+
diff --git a/Layout/default/WarehouseOrder/PDF_MAIN.php b/Layout/default/WarehouseOrder/PDF_MAIN.php
new file mode 100644
index 000000000..144555322
--- /dev/null
+++ b/Layout/default/WarehouseOrder/PDF_MAIN.php
@@ -0,0 +1,133 @@
+setReturnValue(['filename' => $order["id"] . ".pdf"]);
+?>
+
+
+
+
+
Bestellung
+
+
+
+
+
+
+
+
+
XINON Lieferantenbestellung vom =date("d.m.Y", $order["create"])?>
+
+
+
+ | Position |
+ Artikel |
+ Art.-Nr. Lieferant |
+ Menge |
+ Einzelpreis |
+ Gesamtpreis |
+
+
+
+ ">
+ | = $i + 1 ?> |
+ =$p["articleName"]?> |
+ =$p["distributorArticleNumber"]?> |
+ =$p["amount"]?> |
+ =number_format($p["buyPrice"], 2, ",", ".")?> € |
+ =number_format($p["amount"] * $p["buyPrice"], 2, ",", ".")?> € |
+
+
+
+
+
+
+ | Summe |
+ =number_format($sum, 2, ",", ".")?> € |
+
+
+
+
+
+
+
Anmerkungen
+
+ =$order["note"]?>
+
+
+
+
\ No newline at end of file
diff --git a/Layout/default/menu.php b/Layout/default/menu.php
index b670b56fd..0acf0392f 100644
--- a/Layout/default/menu.php
+++ b/Layout/default/menu.php
@@ -49,7 +49,7 @@
- is(["Admin"]) || ($me->is("netowner") && $me->hasGwrNetworks())): ?>
+ is(["Admin"]) || ($me->is("netowner","lineplanner","pipeplanner","pipeworker","lineworker","salespartner"))): ?>
@@ -301,6 +341,7 @@ Vue.component('radius', {
Kundennummer |
Username |
Info |
+
Status |
Aktionen |
@@ -317,6 +358,11 @@ Vue.component('radius', {
{{ user.info }} |
+
+
+
+
+ |
|
@@ -419,10 +465,12 @@ Vue.component('radius', {
custnum: '',
window: window,
showRadacctModal: false,
+ checkOnlineState: 0,
radacctData: null,
}
},
async mounted() {
+ console.log("hallo");
await this.loadFreeUsers();
},
methods: {
@@ -442,7 +490,11 @@ Vue.component('radius', {
});
const response = await fetch(`${window.TT_CONFIG['BASE_PATH']}/Radius/proxyUnsecureHTTPRequestToRadius?${params.toString()}`);
if (response.ok) {
- this.radiusUsers = await response.json();
+ const users = await response.json()
+ if (users.length < 6) {
+ this.checkOnlineState = 1;
+ }
+ this.radiusUsers = users;
} else {
console.error('Failed to load radius users');
}
diff --git a/public/js/pages/WarehouseOffer/WarehouseOffer.css b/public/js/pages/WarehouseOffer/WarehouseOffer.css
new file mode 100644
index 000000000..7951745f0
--- /dev/null
+++ b/public/js/pages/WarehouseOffer/WarehouseOffer.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/public/js/pages/WarehouseOffer/WarehouseOffer.js b/public/js/pages/WarehouseOffer/WarehouseOffer.js
new file mode 100644
index 000000000..fe1365051
--- /dev/null
+++ b/public/js/pages/WarehouseOffer/WarehouseOffer.js
@@ -0,0 +1,178 @@
+Vue.component('warehouse-offer-modal', {
+ props: {
+ id: {type: [String, Number], required: true},
+ mode: {type: String, default: 'edit'}
+ },
+ template: `
+
+ Angebotdetails
+
+
+
+
+
+
Kundenadresse
+
+
+
+
+
+
+
+
+
Positionen
+
+
+
Alternative Artikel
+
+
+
+
+
+
+
+
+
+
+ `,
+ 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('
') : 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: `
+
+
+
+
+
+
+
Notizen
+
{{ row.notes }}
+
Verlauf
+
+ - {{ entry.date }} - {{ entry.description }}
+
+
+
+
+
+ `,
+ data() {
+ return {
+ window: window,
+ offerModalId: null,
+ }
+ },
+});
diff --git a/public/js/pages/WarehouseOrder/WarehouseOrder.css b/public/js/pages/WarehouseOrder/WarehouseOrder.css
new file mode 100644
index 000000000..9b3746d82
--- /dev/null
+++ b/public/js/pages/WarehouseOrder/WarehouseOrder.css
@@ -0,0 +1,52 @@
+.warehouse-order-modal-positions-entry-container {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr 0.5fr 1fr 1fr 0.5fr;
+ grid-gap: 10px;
+}
+
+.warehouse-order-modal-positions-entry-actions {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding-top: 13px;
+}
+
+@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;
+ }
+}
+
+
+/* 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;
+}
+
diff --git a/public/js/pages/WarehouseOrder/WarehouseOrder.js b/public/js/pages/WarehouseOrder/WarehouseOrder.js
index ab63807d5..c9b937d86 100644
--- a/public/js/pages/WarehouseOrder/WarehouseOrder.js
+++ b/public/js/pages/WarehouseOrder/WarehouseOrder.js
@@ -1,70 +1,246 @@
-// noinspection JSUnusedLocalSymbols
-Vue.component('warehouse-order', {
+Vue.component('warehouse-order-modal', {
+ props: {
+ id: {type: [String, Number], required: true},
+ mode: {type: String, default: 'sign'}
+ },
+ template: `
+
+
+
Bestelldetails
+
+
+
+
+
Positionen
+
+
+
+
Lieferadresse
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+
+ data() {
+ return {
+ window: window,
+ positionsConfig: {
+ customOrdering: 'distributorId',
+ fields: {
+ article: {
+ type: 'autocomplete',
+ label: 'Artikel',
+ apiUrl: '/WarehouseArticle/autoComplete',
+ customFieldReference: 'WarehouseArticle',
+ },
+ distributorId: {type: 'select', label: 'Lieferant', options: [], customFieldReference: 'WarehouseDistributor'},
+ distributorArticleNumber: {type: 'input', label: 'Lieferant Art-Nr.'},
+ amount: {type: 'input', label: 'Menge', inputType: 'number'},
+ buyPrice: {type: 'input', label: 'Einkaufspreis', inputType: 'number'},
+ verwendung: {type: 'input', label: 'Verwendung'},
+ },
+ validateForm: (formData) => {
+ const fields = [
+ {key: 'amount', message: 'Bitte füllen Sie die Menge aus'},
+ {key: 'distributorId', message: 'Bitte füllen Sie den Lieferanten aus'},
+ {key: 'article', message: 'Bitte füllen Sie den Artikel aus'},
+ {key: 'buyPrice', message: 'Bitte füllen Sie den Einkaufspreis aus'}
+ ];
+
+ for (const field of fields) {
+ if (!formData[field.key]) {
+ window.notify('error', field.message);
+ return false;
+ }
+ }
+
+ return true;
+ },
+ },
+ order: {
+ extReference: '',
+ delAddrName: 'XINON GmbH',
+ delAddrLine: 'Fladnitz im Raabtal 150',
+ delAddrPLZ: '8322',
+ delAddrCity: 'Studenzen',
+ delAddrEMail: 'einkauf@xinon.at',
+ note: '',
+ editor: window.TT_CONFIG['USER_ID'],
+ positions: [],
+ }
+ }
+ },
+ async mounted() {
+ 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() {
+ if (this.order.positions.length === 0) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
+
+ if (this.id === 'create') {
+ const distributorIds = [...new Set(this.order.positions.map(position => position.distributorId))];
+
+ for (const distributorId of distributorIds) {
+ const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/create`, {
+ ...this.order,
+ distributorId,
+ positions: this.order.positions.filter(position => position.distributorId === distributorId)
+ }
+ );
+ 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('
') : 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) {
+ 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('
') : response.data.message || 'Ein Fehler ist aufgetreten');
+ }
+ },
+ async fetchDistributors(article) {
+ const url = `${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getArticleDistributorData`;
+ const params = typeof article === 'string' ? {allDistributor: true} : {articleId: article};
+
+ const response = await axios.get(url, {params});
+ this.positionsConfig.fields.distributorId.options = response.data.map(distributor => ({
+ value: distributor.id,
+ text: distributor.name,
+ externalArticleNumber: distributor.externalArticleNumber || null,
+ purchasePrice: distributor.purchasePrice || null,
+ }));
+ },
+ async fetchDistributorData(distributorId) {
+ if (distributorId && typeof this.$refs.positionsManager.formData.article === 'number') {
+ const distributor = this.positionsConfig.fields.distributorId.options.find(distributor => parseInt(distributor.value) ===
+ parseInt(distributorId));
+ this.$refs.positionsManager.updateField('distributorArticleNumber', distributor.externalArticleNumber);
+ this.$refs.positionsManager.updateField('buyPrice', distributor.purchasePrice);
+ }
+ },
+ },
+});
+
+Vue.component('warehouse-order-detail', {
//language=Vue
template: `
-
+ Bestellungsdetails für #{{ loading ? 'Laden...' : order.orderNumber }}
-
- {{ window.moment(row.create * 1000).format('DD.MM.YYYY HH:mm:ss') }}
-
-
-
- {{ row.sum.toFixed(2) }} €
-
-
-
-
-
-
-
- -
- {{ item.quantity }}x {{ item.articleName }} - {{ item.price.toFixed(2) }} €
-
-
-
+
+
+
+
+
+ Lieferadresse
+ {{order.delAddrName}}
+ {{order.delAddrEMail}}
+ {{order.delAddrLine}}
+ {{order.delAddrPLZ}} {{order.delAddrCity}}
+
+
+
Artikel
+
Menge
+
Preis
+
Lieferant
+
Verwendung
+
Summe
+
+
+
+
{{ position.articleName }}
+
{{ position.amount }}
+
{{ position.buyPrice }}
+
{{ position.distributorName }}
+
{{ position.verwendung }}
+
{{ position.amount * position.buyPrice }}
+
+
+
+
+ `,
+ 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: `
+
+
+
+
+
+
+ {{ calculateSum(JSON.parse(row["positions"])).toFixed(2)}} €
+
+ {{ row.id % 2 == 0 ? 'Triotronik' : 'Discomp' }}
-
- `, data() {
+ `,
+ data() {
return {
- window: window, historyModal: false, historyModalId: null, observer: null, orderLazyLoad: {},
+ orderModalId: null,
}
- }, mounted() {
- this.observer = new MutationObserver((mutations) => {
- const lazyLoadingElements = document.querySelectorAll('.lazy-loading');
- console.log(lazyLoadingElements);
-
- // check row id and check if it is already defined in orderLazyLoad else alert('loading')
- // if it is defined do nothing
-
- for (const element of lazyLoadingElements) {
- if (element.dataset.rowId in this.orderLazyLoad) {
- continue;
- }
- this.loadOrder(element.dataset.rowId);
- }
-
- })
- this.observer.observe(document.querySelector('.tt-table-container'), {childList: true, subtree: true,});
- }, methods: {
- async loadOrder(rowId) {
- this.orderLazyLoad[rowId] = true;
- // use BASE_PATH . /WarehouseOrder/getOrderItems?id= + rowId
- const response = await axios.post(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseOrder/getOrderItems?id=${rowId}`);
- console.log(response.data);
- this.orderLazyLoad[rowId] = response.data;
-
- // force re-render of the table
- this.$refs.table.$forceUpdate();
-
-
+ },
+ methods: {
+ closeOrderModal() {
+ this.orderModalId = null;
+ this.$refs.table.$refs.table.refreshTable();
+ },
+ calculateSum(positions) {
+ return positions.reduce((sum, position) => sum + position.amount * position.buyPrice, 0);
}
- }, beforeDestroy() {
- this.observer.disconnect();
}
-})
+});
diff --git a/public/js/pages/WarehouseProject/WarehouseProject.css b/public/js/pages/WarehouseProject/WarehouseProject.css
new file mode 100644
index 000000000..7951745f0
--- /dev/null
+++ b/public/js/pages/WarehouseProject/WarehouseProject.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/public/js/pages/WarehouseProject/WarehouseProject.js b/public/js/pages/WarehouseProject/WarehouseProject.js
new file mode 100644
index 000000000..3bfeb5578
--- /dev/null
+++ b/public/js/pages/WarehouseProject/WarehouseProject.js
@@ -0,0 +1,161 @@
+Vue.component('warehouse-project-modal', {
+ props: {
+ id: { type: [String, Number], required: true },
+ mode: { type: String, default: 'edit' }
+ },
+ template: `
+
+
+
Projektübersicht
+
+
+
+
+
Zeitraum
+
+
+
+
+
+
+
Beteiligte Personen
+
+
+
+
+
Projektübersicht
+
+
+
+
+
Lagerort
+
+
+
+
+
+
+ `,
+ 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('
') : 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: `
+
+
+
+
+
+
+
Notizen
+
{{ row.notes }}
+
Verlauf
+
+ - {{ entry.date }} - {{ entry.description }}
+
+
+
+
+
+ `,
+ data() {
+ return {
+ window: window,
+ projectModalId: null,
+ }
+ },
+});
diff --git a/public/plugins/vue/tt-components/css/tt-position-manager.css b/public/plugins/vue/tt-components/css/tt-position-manager.css
new file mode 100644
index 000000000..ed770af35
--- /dev/null
+++ b/public/plugins/vue/tt-components/css/tt-position-manager.css
@@ -0,0 +1,64 @@
+.positions-manager {
+ padding: 1rem;
+ font-family: sans-serif;
+}
+
+.positions-manager .form-group {
+ margin-bottom: 0;
+}
+
+.positions-manager .form-container {
+ display: flex;
+ align-items: center; /* Vertically center */
+ justify-content: flex-start;
+ gap: 1rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid #ddd;
+}
+
+.positions-manager .form-container .button-wrapper {
+ align-self: flex-end; /* Align button container at the bottom */
+}
+
+.positions-manager .form-container [class*="tt-input"],
+.positions-manager .form-container [class*="tt-checkbox"] {
+ flex: 1 1 200px; /* Flexible width with a minimum of 200px */
+ min-width: 200px;
+}
+
+.positions-manager .form-container .btn {
+ align-self: flex-end; /* Align button at the bottom of the form area */
+}
+
+.positions-manager table {
+ width: 100%;
+ margin-top: 1rem;
+ border-collapse: collapse;
+}
+
+.positions-manager table thead th {
+ background-color: #f4f4f4;
+ border-bottom: 2px solid #ddd;
+ padding: 8px;
+ text-align: left;
+}
+
+.positions-manager table th,
+.positions-manager table td {
+ padding: 8px;
+ vertical-align: middle; /* Vertically center text */
+}
+
+.positions-manager table tbody tr:nth-child(even) {
+ background-color: #f9f9f9; /* Alternate row color */
+}
+
+.positions-manager table .btn {
+ margin-right: 0.5rem;
+}
+
+.positions-manager .form-container .btn-sm,
+.positions-manager table .btn.btn-sm {
+ padding: 0.3rem 0.6rem; /* Small button padding */
+ font-size: 0.875rem;
+}
diff --git a/public/plugins/vue/tt-components/tt-autocomplete.js b/public/plugins/vue/tt-components/tt-autocomplete.js
index fc8d9817d..fca5d9bda 100644
--- a/public/plugins/vue/tt-components/tt-autocomplete.js
+++ b/public/plugins/vue/tt-components/tt-autocomplete.js
@@ -42,7 +42,7 @@ Vue.component('tt-autocomplete', {
Einträge werden geladen...
-
+
item.value === this.value);
this.displayValue = selectedItem ? selectedItem.text : '';
} else {
- this.$emit('input', '');
+ if (this.returnText === false && !(typeof this.value === 'undefined' || this.value === '')) this.$emit('input', '');
this.displayValue = this.displayValue.replace(this.oldDisplayValue, '');
}
},
onInput(event) {
this.displayValue = event.target.value;
- this.$emit('input', '');
- this.$emit('input', this.returnText ? this.displayValue : '');
+ if (this.returnText) this.$emit('input', this.displayValue);
this.fetchSuggestions();
}, onFocus() {
this.showSuggestions = true;
@@ -136,10 +141,8 @@ Vue.component('tt-autocomplete', {
this.showSuggestions = false;
}, 200);
}, fetchSuggestions() {
- if (this.displayValue.length < 3) {
- this.$set(this, 'displayingItems', []);
- return this.displayingItems = [];
- }
+ this.$set(this, 'displayingItems', []);
+ if (this.displayValue.length < 3) return;
if (!this.apiUrl) {
diff --git a/public/plugins/vue/tt-components/tt-position-manager.js b/public/plugins/vue/tt-components/tt-position-manager.js
new file mode 100644
index 000000000..7d8996bbd
--- /dev/null
+++ b/public/plugins/vue/tt-components/tt-position-manager.js
@@ -0,0 +1,170 @@
+Vue.component('tt-positions-manager', {
+ props: {
+ value: {type: Array, required: false},
+ config: {type: Object, required: true},
+ },
+ data() {
+ return {
+ window: window,
+ positions: this.value,
+ formData: {},
+ selectedIndex: null,
+ resolvingFields: {},
+ }
+ },
+ template: `
+
+
+
+
+
+
+ | {{ field.label }} |
+ Actions |
+
+
+
+
+ |
+
+
+
+
+ {{ resolvingFields[index + key] }}
+ {{ formatFieldValue(position[key], field) }}
+ |
+
+
+
+ |
+
+
+
+
+ `,
+ methods: {
+ updateField(key, value) {
+ this.$set(this.formData, key, value);
+ },
+ async saveEntry() {
+ 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();
+ },
+ editEntry(index) {
+ this.selectedIndex = index;
+ this.formData = {...this.positions[index]};
+ },
+ deleteEntry(index) {
+ this.positions.splice(index, 1);
+ this.$emit('input', this.positions);
+ },
+ resetForm() {
+ this.formData = {};
+ this.selectedIndex = null;
+ },
+ formatFieldValue(value, field) {
+ if (field.formatter) return field.formatter(value);
+ return value;
+ },
+ async resolveFields() {
+ for (let i = 0; i < this.positions.length; i++) {
+ for (let key in this.config.fields) {
+ if (this.config.fields[key].customFieldResolver) {
+ this.$set(this.resolvingFields, i + key, true);
+ const textValue = await this.config.fields[key].customFieldResolver(this.positions[i][key]);
+ this.$set(this.resolvingFields, i + key, textValue);
+ } else if (this.config.fields[key].customFieldReference) {
+ this.$set(this.resolvingFields, i + key, true);
+ if (this.config.fields[key].customFieldReference) {
+ const entry = await axios.get(window.TT_CONFIG['BASE_PATH'] +
+ '/' +
+ this.config.fields[key].customFieldReference +
+ '/getById?id=' +
+ this.positions[i][key]);
+ const textValue = entry.data.name ?? entry.data.title ?? entry.data.text ?? '[E] Key not found';
+ console.log(textValue);
+ this.$set(this.resolvingFields, i + key, textValue);
+ } else this.$set(this.resolvingFields, i + key, '');
+ }
+ }
+ }
+ }
+ },
+ created() {
+ if (this.config.customMethods) Object.assign(this, this.config.customMethods);
+ },
+ watch: {
+ positions: {
+ handler() {
+ this.resolveFields().then();
+ },
+ deep: true
+ },
+ value: {
+ handler() {
+ this.positions = this.value;
+ },
+ deep: true
+ }
+ }
+});
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/tt-table.js b/public/plugins/vue/tt-components/tt-table.js
index 2677826a4..cde183b3b 100644
--- a/public/plugins/vue/tt-components/tt-table.js
+++ b/public/plugins/vue/tt-components/tt-table.js
@@ -229,6 +229,7 @@ Vue.component('tt-table', {
.isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key])
.format('DD.MM.YYYY HH:mm')) : ''
}}
+ {{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}