-
-
-
Schritt 1: Excel (XLSX) Upload
-
Bitte laden Sie eine XLSX-Datei hoch, die mindestens eine Spalte mit dem Header 'Serial' für die ONT-Seriennummern enthält. Erwartete Spalten (optional, aber zur Anzeige empfohlen): Nummer, Serial, ONT-Type, Meter, Pegel.
-
-
{{ uploadError }}
-
-
-
-
-
-
Schritt 2: Ergebnisse
-
- Ergebnisse herunterladen
-
-
- Neue Datei hochladen
-
-
-
-
-
-
- {{ header }}
- Username
- Kundennummer
- Kundenname
- Info
-
-
-
-
- {{ row[header] }}
- {{ row.fetched_username }}
- {{ row.fetched_customerNumber }}
- {{ row.fetched_customerName }}
- {{ row.fetched_info }}
-
-
-
-
-
-
-
-
-
-
- Loading...
-
-
-
- {{ Math.round(progress) }}%
-
-
-
Verarbeite Zeile {{ currentRow + 1 }} von {{ totalRows }}
-
Aktuelle ONT SN: {{ currentSerial }}
-
-
+
+
+
+
Schritt 1: Excel (XLSX) Upload
+
Bitte laden Sie eine XLSX-Datei hoch, die mindestens eine Spalte mit dem Header 'Serial' für die ONT-Seriennummern enthält. Optional kann eine 'MAC' Spalte für eine alternative Suche verwendet werden.
+
+
{{ uploadError }}
+
+
+
+
+
Schritt 2: Ergebnisse
+
+ Ergebnisse herunterladen
+
+
+ Neue Datei hochladen
+
+
+
+
+
+ {{ header }}
+ Username
+ Kundennummer
+ Kundenname
+ Info
+
+
+
+
+ {{ row[header] }}
+ {{ row.fetched_username }}
+ {{ row.fetched_customerNumber }}
+ {{ row.fetched_customerName }}
+ {{ row.fetched_info }}
+
+
+
+
+
+
+
+
+
+
+ Loading...
+
+
+
+ {{ Math.round(progress) }}%
+
+
+
Verarbeite Zeile {{ currentRow + 1 }} von {{ totalRows }}
+
Aktuelle Suche: {{ currentSerial }}
+
+
+
`,
data() {
return {
- step: 1, // 1: Upload, 2: Results
- parsedData: [], // Raw data from XLSX
- processedData: [], // Data after API calls
- originalHeaders: [], // Headers from the uploaded XLSX
+ step: 1,
+ parsedData: [],
+ processedData: [],
+ originalHeaders: [],
loading: false,
progress: 0,
currentRow: 0,
totalRows: 0,
- currentSerial: '', // Track the serial being processed for display
- uploadError: null, // To display errors during upload/parsing
- // Define the key column name expected in the XLSX for the ONT Serial Number
- serialColumnName: 'Serial', // IMPORTANT: Adjust if the header name in the XLSX is different
- // Define keys for the fetched data to avoid conflicts with original headers
+ currentSerial: '',
+ uploadError: null,
+ serialColumnName: 'Serial',
+ macColumnName: 'MAC',
fetchedKeys: {
username: 'fetched_username',
customerNumber: 'fetched_customerNumber',
customerName: 'fetched_customerName',
info: 'fetched_info'
},
- // Base path for the API - ensure TT_CONFIG is available globally
- apiBasePath: window.TT_CONFIG ? window.TT_CONFIG['BASE_PATH'] : '/default/path/to/api' // Provide a fallback or handle error if TT_CONFIG is missing
+ apiBasePath: window.TT_CONFIG?.BASE_PATH
};
},
methods: {
- /**
- * Resets the component state to allow a new file upload.
- */
resetComponent() {
- this.step = 1;
- this.parsedData = [];
- this.processedData = [];
- this.originalHeaders = [];
- this.loading = false;
- this.progress = 0;
- this.currentRow = 0;
- this.totalRows = 0;
- this.currentSerial = '';
- this.uploadError = null;
- // Reset the file input visually (optional, requires ref)
+ Object.assign(this.$data, this.$options.data.call(this));
const input = this.$el.querySelector('input[type="file"]');
- if (input) {
- input.value = '';
- }
+ if (input) input.value = '';
},
- /**
- * Handles the file input change event.
- * Loads the XLSX library if needed, reads the file,
- * parses the data, validates the 'Serial' column,
- * and triggers processing.
- * @param {Event} event - The file input change event.
- */
async handleFileUpload(event) {
const file = event.target.files[0];
- this.uploadError = null; // Clear previous errors
+ this.uploadError = null;
if (!file) return;
- this.loading = true; // Show loading indicator early
+ this.loading = true;
try {
- // Load XLSX library dynamically if not already loaded
await this.loadXLSX();
+ const data = await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = e => resolve(new Uint8Array(e.target.result));
+ reader.onerror = () => reject(new Error("Fehler beim Lesen der Datei."));
+ reader.readAsArrayBuffer(file);
+ });
- const reader = new FileReader();
- reader.onload = (e) => {
- try {
- const data = new Uint8Array(e.target.result);
- const workbook = XLSX.read(data, { type: "array" });
- const worksheetName = workbook.SheetNames[0];
- const worksheet = workbook.Sheets[worksheetName];
+ const workbook = XLSX.read(data, { type: 'array' });
+ const worksheet = workbook.Sheets[workbook.SheetNames[0]];
+ this.parsedData = XLSX.utils.sheet_to_json(worksheet, { defval: "" });
- // Parse the sheet into an array of objects, automatically detecting headers
- this.parsedData = XLSX.utils.sheet_to_json(worksheet, { defval: "" }); // Use defval to handle empty cells
+ if (!this.parsedData.length) {
+ throw new Error("Die hochgeladene Datei ist leer oder konnte nicht gelesen werden.");
+ }
- if (this.parsedData.length === 0) {
- this.uploadError = "Die hochgeladene Datei ist leer oder konnte nicht gelesen werden.";
- this.loading = false;
- return;
- }
-
- // Get headers from the first row of parsed data
- this.originalHeaders = Object.keys(this.parsedData[0]);
-
- // --- Validation: Check if the required 'Serial' column exists ---
- if (!this.originalHeaders.includes(this.serialColumnName)) {
- this.uploadError = `Fehler: Die erforderliche Spalte '${this.serialColumnName}' wurde in der hochgeladenen Datei nicht gefunden. Gefundene Spalten: ${this.originalHeaders.join(', ')}`;
- this.loading = false;
- this.parsedData = []; // Clear data if invalid
- this.originalHeaders = [];
- // Keep step at 1 to show the error
- return;
- }
- // --- End Validation ---
-
- // If validation passes, proceed to processing
- this.startProcessing();
-
- } catch (parseError) {
- console.error("Error parsing XLSX file:", parseError);
- this.uploadError = `Fehler beim Verarbeiten der XLSX-Datei: ${parseError.message}`;
- this.loading = false;
- this.step = 1; // Stay on upload step
- }
- };
- reader.onerror = (err) => {
- console.error("FileReader error:", err);
- this.uploadError = "Fehler beim Lesen der Datei.";
- this.loading = false;
- this.step = 1;
- };
- reader.readAsArrayBuffer(file);
-
- } catch (libLoadError) {
- console.error("Error loading XLSX library:", libLoadError);
- this.uploadError = "Fehler beim Laden der erforderlichen Bibliothek (xlsx). Bitte versuchen Sie es erneut.";
+ this.originalHeaders = Object.keys(this.parsedData[0]);
+ if (!this.originalHeaders.includes(this.serialColumnName)) {
+ throw new Error(`Erforderliche Spalte '${this.serialColumnName}' nicht gefunden.`);
+ }
+ this.startProcessing();
+ } catch (error) {
+ console.error("File processing error:", error);
+ this.uploadError = error.message;
this.loading = false;
this.step = 1;
}
},
- /**
- * Dynamically loads the SheetJS (XLSX) library if it's not already available.
- */
async loadXLSX() {
- if (!window.XLSX) {
- console.log("Loading XLSX library...");
- await new Promise((resolve, reject) => {
- const script = document.createElement('script');
- script.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js'; // Consider using a newer version if available
- script.async = true;
- script.onload = () => {
- console.log("XLSX library loaded.");
- resolve();
- };
- script.onerror = (err) => {
- console.error("Failed to load XLSX script:", err);
- reject(new Error("Could not load XLSX library"));
- };
- document.head.appendChild(script);
- });
- }
+ if (window.XLSX) return;
+ return new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js';
+ script.async = true;
+ script.onload = resolve;
+ script.onerror = () => reject(new Error("Could not load XLSX library."));
+ document.head.appendChild(script);
+ });
},
- /**
- * Processes the parsed data row by row, fetching details from the Radius API.
- */
async startProcessing() {
this.loading = true;
this.totalRows = this.parsedData.length;
- this.processedData = []; // Clear previous results
+ this.processedData = [];
this.progress = 0;
this.currentRow = 0;
- this.currentSerial = '';
- const apiUrlBase = `${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?ont_sn=`;
- const notFoundMessage = 'N/A - Keinen Benutzer mit dieser ONT SN gefunden';
+ const snApiUrlBase = `${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?ont_sn=`;
+ const macApiUrlBase = `${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?username=`;
+ const sesApiUrlBase = `${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?action2=find_by_current_session&mac=`;
- for (let i = 0; i < this.parsedData.length; i++) {
+ const setRowStatus = (row, msg, data = {}) => {
+ const defaultData = { username: `N/A - ${msg}`, customerNumber: 'N/A', customerName: 'N/A', info: 'N/A' };
+ Object.keys(this.fetchedKeys).forEach(key => row[this.fetchedKeys[key]] = data[key] || defaultData[key]);
+ };
+
+ for (const [i, row] of this.parsedData.entries()) {
this.currentRow = i;
- const row = { ...this.parsedData[i] }; // Create a copy to avoid modifying original parsed data
- const serialNumber = row[this.serialColumnName]?.trim(); // Get serial number, trim whitespace
-
- this.currentSerial = serialNumber || 'Leer'; // Update display
- this.progress = ((i + 1) / this.totalRows) * 100;
-
- // Initialize fetched data fields
- row[this.fetchedKeys.username] = '';
- row[this.fetchedKeys.customerNumber] = '';
- row[this.fetchedKeys.customerName] = '';
- row[this.fetchedKeys.info] = '';
+ const newRow = { ...row };
+ const serialNumber = row[this.serialColumnName]?.trim();
+ this.currentSerial = `SN: ${serialNumber || 'Leer'}`;
if (!serialNumber) {
- // Handle rows with empty serial numbers
- row[this.fetchedKeys.username] = 'N/A - Leere Seriennummer';
- row[this.fetchedKeys.customerNumber] = 'N/A';
- row[this.fetchedKeys.customerName] = 'N/A';
- row[this.fetchedKeys.info] = 'N/A';
- this.processedData.push(row);
- await this.sleep(10); // Small delay for UI update even for empty rows
- continue; // Move to the next row
+ setRowStatus(newRow, 'Leere Seriennummer');
+ this.processedData.push(newRow);
+ this.progress = ((i + 1) / this.totalRows) * 100;
+ continue;
}
- try {
- const response = await fetch(apiUrlBase + encodeURIComponent(serialNumber));
- if (!response.ok) {
- // Handle HTTP errors (e.g., 404, 500)
- console.error(`API Error for SN ${serialNumber}: ${response.status} ${response.statusText}`);
- row[this.fetchedKeys.username] = `N/A - API Fehler (${response.status})`;
- row[this.fetchedKeys.customerNumber] = 'N/A';
- row[this.fetchedKeys.customerName] = 'N/A';
- row[this.fetchedKeys.info] = 'N/A';
- } else {
- const data = await response.json();
+ let found = false;
- if (Array.isArray(data) && data.length > 0) {
- // Assuming the first result is the relevant one if multiple are returned
- const userData = data[0];
- row[this.fetchedKeys.username] = userData.username || 'N/A';
- row[this.fetchedKeys.customerNumber] = userData.customerNumber || 'N/A';
- row[this.fetchedKeys.customerName] = userData.customerName || 'N/A';
- row[this.fetchedKeys.info] = userData.info || 'N/A';
- } else {
- // Handle case where API returns success but an empty array or unexpected format
- row[this.fetchedKeys.username] = notFoundMessage;
- row[this.fetchedKeys.customerNumber] = 'N/A';
- row[this.fetchedKeys.customerName] = 'N/A';
- row[this.fetchedKeys.info] = 'N/A';
+ try {
+ const snResponse = await fetch(snApiUrlBase + encodeURIComponent(serialNumber));
+ if (snResponse.ok) {
+ const snData = await snResponse.json();
+ if (snData?.length > 0) {
+ setRowStatus(newRow, '', snData[0]);
+ found = true;
}
}
} catch (error) {
- console.error(`Error fetching data for SN ${serialNumber}:`, error);
- row[this.fetchedKeys.username] = 'N/A - Fehler bei API-Abfrage';
- row[this.fetchedKeys.customerNumber] = 'N/A';
- row[this.fetchedKeys.customerName] = 'N/A';
- row[this.fetchedKeys.info] = 'N/A';
+ console.error(`Fetch error for SN ${serialNumber}:`, error);
}
- this.processedData.push(row);
+ if (!found && this.originalHeaders.includes(this.macColumnName)) {
+ const macAddress = row[this.macColumnName]?.trim();
+ this.currentSerial = `MAC: ${macAddress || 'Leer'}`;
+ if (macAddress && macAddress.length === 12) {
+ const formattedMac = macAddress.toUpperCase().match(/.{1,2}/g).join(':');
+ try {
+ const sesResponse = await fetch(`${sesApiUrlBase}${encodeURIComponent(formattedMac)}`);
+ if (sesResponse.ok) {
+ const sesData = await sesResponse.json();
+ if (sesData?.length === 0) continue;
- // Optional small delay to prevent UI freeze on large files and allow progress update
- if (i % 20 === 0) { // Update UI roughly every 20 rows
- await this.sleep(20);
+ const username = sesData[0];
+
+ const macResponse = await fetch(`${macApiUrlBase}${encodeURIComponent(username)}&info=&custnum=`);
+ if (macResponse.ok) {
+ const macData = await macResponse.json();
+ if (macData?.length > 0) {
+ setRowStatus(newRow, '', macData[0]);
+ console.log("found via MAC:", formattedMac, macData[0]);
+ found = true;
+ }
+ }
+ }
+
+
+
+ } catch (error) {
+ console.error(`Fetch error for MAC ${formattedMac}:`, error);
+ }
+ }
}
+
+ if (!found) {
+ setRowStatus(newRow, 'Keinen Benutzer gefunden');
+ }
+
+ this.processedData.push(newRow);
+ this.progress = ((i + 1) / this.totalRows) * 100;
+ if ((i + 1) % 20 === 0) await this.sleep(20);
}
this.loading = false;
- this.step = 2; // Move to results view
- this.currentSerial = ''; // Clear serial display
+ this.step = 2;
+ this.currentSerial = '';
},
- /**
- * Creates and triggers the download of an XLSX file containing the processed results.
- */
downloadResults() {
if (!this.processedData.length) return;
-
try {
- // Prepare data for export: Select and order columns
const dataToExport = this.processedData.map(row => {
const exportRow = {};
- // Include original columns first
- this.originalHeaders.forEach(header => {
- exportRow[header] = row[header];
- });
- // Add fetched data with user-friendly headers
+ this.originalHeaders.forEach(header => { exportRow[header] = row[header]; });
exportRow['Username'] = row[this.fetchedKeys.username];
exportRow['Kundennummer'] = row[this.fetchedKeys.customerNumber];
exportRow['Kundenname'] = row[this.fetchedKeys.customerName];
@@ -876,47 +800,25 @@ Vue.component('radius-ont-finder', {
return exportRow;
});
-
const ws = XLSX.utils.json_to_sheet(dataToExport);
const wb = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(wb, ws, "ONT_Finder_Results"); // Sheet name
-
- // Generate filename (e.g., results_YYYYMMDD_HHMMSS.xlsx)
+ XLSX.utils.book_append_sheet(wb, ws, "ONT_Finder_Results");
const timestamp = new Date().toISOString().replace(/[-:.]/g, "").slice(0, 14);
- const filename = `ont_finder_results_${timestamp}.xlsx`;
-
- XLSX.writeFile(wb, filename);
+ XLSX.writeFile(wb, `ont_finder_results_${timestamp}.xlsx`);
} catch (error) {
console.error("Error generating results file:", error);
- alert("Fehler beim Erstellen der Excel-Datei für den Download."); // Simple alert for user feedback
+ alert("Fehler beim Erstellen der Excel-Datei für den Download.");
}
},
- /**
- * Utility function to pause execution for a specified duration.
- * Useful for allowing UI updates during long loops.
- * @param {number} ms - Milliseconds to sleep.
- */
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
- }
+ },
},
- /**
- * Lifecycle hook called when the component is mounted.
- * Checks if the global TT_CONFIG is available.
- */
mounted() {
- if (!window.TT_CONFIG || !window.TT_CONFIG['BASE_PATH']) {
- console.warn("Global TT_CONFIG or TT_CONFIG['BASE_PATH'] not found. API calls may fail. Using fallback path:", this.apiBasePath);
- // Optionally display a warning to the user
- // this.uploadError = "Konfiguration für API-Pfad nicht gefunden. Funktionalität möglicherweise beeinträchtigt.";
- } else {
- // Update apiBasePath if TT_CONFIG was found after initial data setup (less likely but safe)
- this.apiBasePath = window.TT_CONFIG['BASE_PATH'];
+ if (!window.TT_CONFIG?.BASE_PATH) {
+ console.warn(`Global TT_CONFIG.BASE_PATH not found. API calls will use fallback path: ${this.apiBasePath}`);
}
- // Ensure XLSX is loaded once the component is ready, in case it's needed immediately
- // Although handleFileUpload loads it, pre-loading might be slightly smoother if needed elsewhere later
- // this.loadXLSX().catch(err => console.error("Pre-loading XLSX library failed:", err));
}
});
diff --git a/public/js/pages/WarehouseArticlePacket/WarehouseArticlePacket.js b/public/js/pages/WarehouseArticlePacket/WarehouseArticlePacket.js
index a6fdda079..dbf08a22d 100644
--- a/public/js/pages/WarehouseArticlePacket/WarehouseArticlePacket.js
+++ b/public/js/pages/WarehouseArticlePacket/WarehouseArticlePacket.js
@@ -37,6 +37,21 @@ Vue.component('WarehouseArticlePacket', {
articles: [],
}
}, beforeMount() {
+ // Dynamically filter articles based on the user's shop context
+ // This assumes TT_CONFIG is available and contains userAddressId
+ const userAddressId = window['TT_CONFIG']['userAddressId'];
+ let articleFilter = {};
+ if (userAddressId === 209) {
+ articleFilter = { isEShop: 1 };
+ } else if (userAddressId === 210) {
+ articleFilter = { isSbidiShop: 1 };
+ }
+
+ // The current implementation directly uses `window['TT_CONFIG']['CRUD_CONFIG'].columns.find(...)`
+ // which might not reflect the filtered articles.
+ // To properly filter, the `input-article` component or its underlying API call needs to be aware of the shop context.
+ // For now, this will just get all articles that were passed from the backend during initial config.
+ // A more robust solution would involve modifying the `WarehouseArticle/autocomplete` API to accept shop filters.
this.articles = window['TT_CONFIG']['CRUD_CONFIG'].columns.find(column => column.key === 'subItems').modal.items;
}
})
diff --git a/public/js/pages/WarehouseEShop/WarehouseEShop.js b/public/js/pages/WarehouseEShop/WarehouseEShop.js
index 5a6168991..509157109 100644
--- a/public/js/pages/WarehouseEShop/WarehouseEShop.js
+++ b/public/js/pages/WarehouseEShop/WarehouseEShop.js
@@ -1,53 +1,53 @@
Vue.component('warehouse-e-shop', {
//language=Vue
template: `
-
-
+
+
-
-
-
+
+
-
+ ]" sm row/>
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
- {{ row.calculatedSellPrice.toFixed(2) }} €
- {{
- Array.isArray(JSON.parse(row.cheapestSellPrice)) ? JSON.parse(row.cheapestSellPrice).find(price => price.title === 'Energie Steiermark').price.toFixed(2) :
- Object.values(JSON.parse(row.cheapestSellPrice)).find(price => price.title === 'Energie Steiermark').price.toFixed(2) }} €
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
- `, data() {
+
+ {{ row.calculatedSellPrice.toFixed(2) }} €
+
+ {{ getArticlePrice(row).toFixed(2) }} €
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `, data() {
return {
window: window, itemAmounts: {}, shoppingCart: [], createOrderDialog: false, createOrderDialogData: {
deliveryMode: 'singleAddress',
@@ -59,9 +59,29 @@ Vue.component('warehouse-e-shop', {
deliveryAddressPLZ: '',
deliveryAddressCity: '',
},
+ userAddressId: window['TT_CONFIG']['userAddressId'] || null, // Get user's address ID from PHP
}
},
methods: {
+ getArticlePrice(row) {
+ const cheapestSellPrice = JSON.parse(row.cheapestSellPrice);
+ let priceTitle = '';
+
+ if (this.userAddressId === 209) {
+ priceTitle = 'Energie Steiermark';
+ } else if (this.userAddressId === 9633) {
+ priceTitle = 'SBIDI';
+ } else {
+ // Default or error handling if addressId is not recognized
+ return 0;
+ }
+
+ const foundPrice = Array.isArray(cheapestSellPrice)
+ ? cheapestSellPrice.find(price => price.title === priceTitle)
+ : Object.values(cheapestSellPrice).find(price => price.title === priceTitle);
+
+ return foundPrice ? foundPrice.price : 0;
+ },
async openOrderDialog() {
this.createOrderDialog = true;
},
@@ -81,6 +101,7 @@ Vue.component('warehouse-e-shop', {
if (response.data.success) {
this.window.notify('success', response.data.message || 'Erfolgreich gespeichert');
this.shoppingCart = [];
+ this.itemAmounts = {}; // Clear item amounts after successful order
this.createOrderDialogData = {
deliveryMode: 'singleAddress',
extRef: '',
diff --git a/public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js b/public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js
index a66d6b0c5..ac100e128 100644
--- a/public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js
+++ b/public/js/pages/WarehouseEShopOrder/WarehouseEShopOrder.js
@@ -45,7 +45,7 @@ Vue.component('warehouse-e-shop-order-modal-positions-mgmt', {
}
for (const response of articlePacketResponses) {
- this.$set(this.articlePacketNames, response.data[0].value, response.data[0].text);
+ this.$set(this.articlePacketNames, response.data[0].value, response.data[0].data[0].text); // Adjusted for packet autocomplete response structure
}
},
@@ -109,47 +109,47 @@ Vue.component('warehouse-e-shop-order-modal-positions-mgmt', {
},
}, watch: {positions: {handler: 'fetchNames', immediate: true}}, //language=Vue
template: `
-
+
-
+
-
-
-
-
- Artikel
- Menge
- Aktionen
-
-
-
-
- Keine Einträge
-
-
- {{ position.articleId ? articleNames[position.articleId] : position.articlePacketId ? articlePacketNames[position.articlePacketId] :
- 'Loading...' }}
-
- {{ position.quantity }}
-
- Löschen
- Bearbeiten
-
-
-
-
-
+
+
+
+
+ Artikel
+ Menge
+ Aktionen
+
+
+
+
+ Keine Einträge
+
+
+ {{ position.articleId ? articleNames[position.articleId] : position.articlePacketId ? articlePacketNames[position.articlePacketId] :
+ 'Loading...' }}
+
+ {{ position.quantity }}
+
+ Löschen
+ Bearbeiten
+
+
+
+
+
-
- `,
+
+ `,
})
@@ -157,83 +157,89 @@ Vue.component('warehouse-e-shop-order-modal-positions-mgmt', {
Vue.component('warehouse-e-shop-order', {
//language=Vue
template: `
-
-
+
+
-
-
-
- Excel Export für neue Bestellungen
-
-
+
+
+
+ Excel Export für neue Bestellungen
+
+
-
- {{ row.deliveryAddressName }} {{ row.deliveryAddressAdditional ? '(' + row.deliveryAddressAdditional + ')' : '' }}
-
+
+ {{ row.deliveryAddressName }} {{ row.deliveryAddressAdditional ? '(' + row.deliveryAddressAdditional + ')' : '' }}
+
-
- {{ window.moment(row.create * 1000).format('DD.MM.YYYY HH:mm:ss') }}
-
+
+ {{ window.moment(row.create * 1000).format('DD.MM.YYYY HH:mm:ss') }}
+
-
-
-
-
- Menge: {{ item.quantity }} | {{ item.articlePacketTitle || item.articleTitle }}
-
-
-
-
+
+ Energie Steiermark
+ SBIDI
+ Unbekannt
+
-
-
-
-
-
+
+
+
+
+ Menge: {{ item.quantity }} | {{ item.articlePacketTitle || item.articleTitle }}
+
+
+
+
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
-
-
-
-
- {{ entry.date }} {{ entry.time }} - {{ entry.evtDscr }}
-
-
-
+
+
+
+
+
+
+
+
-
+
+
+
+
+ {{ entry.date }} {{ entry.time }} - {{ entry.evtDscr }}
+
+
+
+
+
-
- `, data() {
+
+ `, data() {
return {
window: window,
historyModal: false,
diff --git a/public/plugins/bookstack/bookstackIntegration.js b/public/plugins/bookstack/bookstackIntegration.js
index 23abfdde0..59523a5bb 100644
--- a/public/plugins/bookstack/bookstackIntegration.js
+++ b/public/plugins/bookstack/bookstackIntegration.js
@@ -1,50 +1,51 @@
document.addEventListener('DOMContentLoaded', async () => {
- const articleTag = (() => {
- const path = window.location.pathname;
- const segments = path.split('/').filter(Boolean);
- return segments.length > 0 ? segments[0] : 'DefaultTag';
- })();
+ const linkEl = document.getElementById('bookstackLink');
+ if (!linkEl) return;
+ const articleTag = window.location.pathname.split('/').filter(Boolean)[0] || 'DefaultTag';
+ const cacheKey = `bookstack_article_${articleTag}`;
- const apiUrl = `https://bookstack.xinon.at/api/search?query=%5Bshowurl%3D${encodeURIComponent(articleTag)}%5D%7Btype%3Apage%7D`;
- const linkElement = document.getElementById('bookstackLink');
+ const setupLinkAction = url => {
+ linkEl.style.display = 'block';
+ linkEl.querySelector('a').onclick = e => {
+ e.preventDefault();
+ const modal = document.createElement('div');
+ modal.className = 'bookstack-integration-modal';
+ modal.innerHTML = `
×
`;
+ modal.onclick = ev => {
+ if (ev.target === modal || ev.target.classList.contains('bookstack-integration-close-btn')) {
+ modal.remove();
+ }
+ };
+ document.body.appendChild(modal);
+ };
+ };
+
+ document.addEventListener('keydown', e => {
+ if (e.ctrlKey && e.key === 'F8') {
+ e.preventDefault();
+ localStorage.removeItem(cacheKey);
+ window.notify('success', `📗 BookStack cache für '${articleTag}' wurde gelöscht.`);
+ }
+ });
try {
- const response = await fetch(apiUrl, {
- headers: {
- 'Authorization': 'Token XmGSDWlg3bZhHKXFchNXQ9LpXvCaBuM1:k6XNe6RUU1BIxkv5pxpZ9PSErqZbHJ4i'
- }
+ const cachedItem = JSON.parse(localStorage.getItem(cacheKey) || 'null');
+ if (cachedItem && (Date.now() - cachedItem.timestamp < (cachedItem.url ? 604800000 : 259200000))) {
+ if (cachedItem.url) setupLinkAction(cachedItem.url);
+ return;
+ }
+
+ const response = await fetch(`https://bookstack.xinon.at/api/search?query=%5Bshowurl%3D${encodeURIComponent(articleTag)}%5D%7Btype%3Apage%7D`, {
+ headers: { 'Authorization': 'Token XmGSDWlg3bZhHKXFchNXQ9LpXvCaBuM1:k6XNe6RUU1BIxkv5pxpZ9PSErqZbHJ4i' }
});
const data = await response.json();
+ const articleUrl = data.data?.[0]?.url || null;
- if (data.data && data.data.length > 0) {
- const article = data.data[0];
- linkElement.style.display = 'block';
- linkElement.querySelector('a').addEventListener('click', (e) => {
- e.preventDefault();
- showArticleModal(article.url);
- });
- }
+ localStorage.setItem(cacheKey, JSON.stringify({ url: articleUrl, timestamp: Date.now() }));
+ articleUrl ? setupLinkAction(articleUrl) : (linkEl.style.display = 'none');
} catch (error) {
console.error('BookStack API error:', error);
- linkElement.style.display = 'none';
+ linkEl.style.display = 'none';
}
-
- function showArticleModal(url) {
- const modal = document.createElement('div');
- modal.className = 'bookstack-integration-modal';
- modal.innerHTML = `
-
- ×
-
-
- `;
-
- modal.querySelector('.bookstack-integration-close-btn').addEventListener('click', () => modal.remove());
- modal.addEventListener('click', (e) => {
- if (e.target === modal) modal.remove();
- });
-
- document.body.appendChild(modal);
- }
-});
+});
\ No newline at end of file
diff --git a/public/plugins/bookstack/bookstackIntegration.min.js b/public/plugins/bookstack/bookstackIntegration.min.js
new file mode 100644
index 000000000..97df3ee40
--- /dev/null
+++ b/public/plugins/bookstack/bookstackIntegration.min.js
@@ -0,0 +1 @@
+document.addEventListener("DOMContentLoaded",(async()=>{const t=document.getElementById("bookstackLink");if(!t)return;const e=window.location.pathname.split("/").filter(Boolean)[0]||"DefaultTag",o=`bookstack_article_${e}`,a=e=>{t.style.display="block",t.querySelector("a").onclick=t=>{t.preventDefault();const o=document.createElement("div");o.className="bookstack-integration-modal",o.innerHTML=`
×
`,o.onclick=t=>{(t.target===o||t.target.classList.contains("bookstack-integration-close-btn"))&&o.remove()},document.body.appendChild(o)}};document.addEventListener("keydown",(t=>{t.ctrlKey&&"F8"===t.key&&(t.preventDefault(),localStorage.removeItem(o),window.notify("success",`📗 BookStack cache für '${e}' wurde gelöscht.`))}));try{const n=JSON.parse(localStorage.getItem(o)||"null");if(n&&Date.now()-n.timestamp<(n.url?6048e5:2592e5))return void(n.url&&a(n.url));const c=await fetch(`https://bookstack.xinon.at/api/search?query=%5Bshowurl%3D${encodeURIComponent(e)}%5D%7Btype%3Apage%7D`,{headers:{Authorization:"Token XmGSDWlg3bZhHKXFchNXQ9LpXvCaBuM1:k6XNe6RUU1BIxkv5pxpZ9PSErqZbHJ4i"}}),r=await c.json(),s=r.data?.[0]?.url||null;localStorage.setItem(o,JSON.stringify({url:s,timestamp:Date.now()})),s?a(s):t.style.display="none"}catch(e){console.error("BookStack API error:",e),t.style.display="none"}}));
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/css/tt-file-gallery.css b/public/plugins/vue/tt-components/css/tt-file-gallery.css
new file mode 100644
index 000000000..d0632e44c
--- /dev/null
+++ b/public/plugins/vue/tt-components/css/tt-file-gallery.css
@@ -0,0 +1,181 @@
+.tt-file-gallery-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: 1rem;
+}
+
+.tt-file-gallery-item {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ cursor: pointer;
+}
+
+.tt-file-gallery-thumbnail {
+ width: 100%;
+ height: 100px;
+ object-fit: cover;
+ border-radius: 0.25rem;
+ border: 1px solid #dee2e6;
+ transition: transform 0.2s;
+}
+
+.tt-file-gallery-item:hover .tt-file-gallery-thumbnail {
+ transform: scale(1.05);
+}
+
+.tt-file-gallery-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 40px; /* Adjust to not cover filename */
+ background: rgba(0, 0, 0, 0.4);
+ color: white;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ opacity: 0;
+ transition: opacity 0.2s;
+ pointer-events: none;
+ border-radius: 0.25rem;
+}
+
+.tt-file-gallery-item:hover .tt-file-gallery-overlay {
+ opacity: 1;
+}
+
+.tt-file-gallery-icon-container {
+ width: 100%;
+ height: 100px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 1px solid #dee2e6;
+ border-radius: 0.25rem;
+ background-color: #f8f9fa;
+ text-decoration: none;
+ color: inherit;
+}
+
+.tt-file-gallery-filename {
+ font-size: 0.8rem;
+ margin-top: 0.5rem;
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 0 4px;
+}
+
+/* --- Fullscreen Viewer Styles --- */
+
+.tt-fullscreen-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.85);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ z-index: 9999;
+ outline: none;
+}
+
+.tt-fullscreen-toolbar {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 15px;
+ display: flex;
+ gap: 15px;
+ z-index: 10001;
+}
+
+.tt-fullscreen-btn {
+ background: rgba(0, 0, 0, 0.3);
+ border: none;
+ color: white;
+ font-size: 1.5rem;
+ cursor: pointer;
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-decoration: none;
+ transition: background-color 0.2s;
+}
+
+.tt-fullscreen-btn:hover {
+ background: rgba(0, 0, 0, 0.6);
+}
+
+.tt-fullscreen-content {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.tt-fullscreen-image-wrapper {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden; /* Important for panning */
+}
+
+.tt-fullscreen-image {
+ max-width: 95vw;
+ max-height: 95vh;
+ object-fit: contain;
+ will-change: transform; /* Performance hint for browser */
+}
+
+.tt-fullscreen-pdf {
+ width: calc(100vw - 40px);
+ height: calc(100vh - 40px);
+ max-width: 1600px; /* Optional: max width for very large screens */
+ border: none;
+ background-color: white;
+}
+
+.tt-fullscreen-nav-btn {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ background: rgba(0, 0, 0, 0.3);
+ border: none;
+ color: white;
+ font-size: 2rem;
+ cursor: pointer;
+ padding: 10px;
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: background-color 0.2s;
+}
+
+.tt-fullscreen-nav-btn:hover {
+ background: rgba(0, 0, 0, 0.6);
+}
+
+.tt-fullscreen-nav-btn.left {
+ left: 15px;
+}
+
+.tt-fullscreen-nav-btn.right {
+ right: 15px;
+}
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/css/tt-tooltip.css b/public/plugins/vue/tt-components/css/tt-tooltip.css
index f33131e51..f180459ec 100644
--- a/public/plugins/vue/tt-components/css/tt-tooltip.css
+++ b/public/plugins/vue/tt-components/css/tt-tooltip.css
@@ -18,7 +18,7 @@
text-align: center; /* Center text */
}
-/* Make tooltip visible when showTooltip is true */
+/* Make tooltip visible on hover */
.tt-tooltip-wrapper:hover .tt-tooltip-box {
opacity: 1;
}
@@ -89,7 +89,8 @@
border-color: transparent #333 transparent transparent;
}
+/* The problematic 'width: 100% !important;' has been removed from the selector below.
+*/
.tt-tooltip-wrapper > * {
display: inline-block; /* Ensure the tooltip wrapper behaves correctly */
- width: 100% !important;
}
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/tt-checkbox.js b/public/plugins/vue/tt-components/tt-checkbox.js
index 40f501de1..f76a45476 100644
--- a/public/plugins/vue/tt-components/tt-checkbox.js
+++ b/public/plugins/vue/tt-components/tt-checkbox.js
@@ -18,9 +18,6 @@ Vue.component('tt-checkbox', {
this.checkedValue = val;
}
},
- mounted() {
- this.$emit('input', this.checkedValue);
- },
template: `
diff --git a/public/plugins/vue/tt-components/tt-file-gallery.js b/public/plugins/vue/tt-components/tt-file-gallery.js
new file mode 100644
index 000000000..0a4e677f6
--- /dev/null
+++ b/public/plugins/vue/tt-components/tt-file-gallery.js
@@ -0,0 +1,233 @@
+Vue.component('tt-file-gallery', {
+ props: {
+ files: { type: Array, default: () => [] }
+ },
+ data() {
+ return {
+ fullscreenItem: null, // Holds the file being viewed
+ currentImageIndex: 0,
+
+ // Zoom & Pan state
+ zoom: 1,
+ pan: { x: 0, y: 0 },
+ isPanning: false,
+ panStart: { x: 0, y: 0 },
+ lastPinchDist: 0,
+ }
+ },
+ computed: {
+ imageFiles() {
+ return this.files.filter(this.isImage);
+ },
+ isViewingImage() {
+ return this.fullscreenItem && this.isImage(this.fullscreenItem);
+ },
+ imageTransformStyle() {
+ // Apply CSS transform for zoom and pan
+ const { x, y } = this.pan;
+ return {
+ transform: `translate(${x}px, ${y}px) scale(${this.zoom})`,
+ cursor: this.isPanning ? 'grabbing' : 'grab',
+ transition: this.isPanning ? 'none' : 'transform 0.2s',
+ };
+ },
+ fullscreenDownloadUrl() {
+ if (!this.fullscreenItem) return '#';
+ return `/File/download?id=${this.fullscreenItem.id}`;
+ }
+ },
+ methods: {
+ // File type checks
+ isImage(file) {
+ return file.mimetype && file.mimetype.startsWith('image/');
+ },
+ isPdf(file) {
+ return file.mimetype === 'application/pdf';
+ },
+
+ // Get icon for non-image/pdf files
+ getFileIcon(file) {
+ const extension = file.fileName?.split('.').pop().toLowerCase();
+ switch (extension) {
+ case 'doc':
+ case 'docx': return 'fas fa-file-word text-primary';
+ case 'xls':
+ case 'xlsx': return 'fas fa-file-excel text-success';
+ case 'zip':
+ case 'rar': return 'fas fa-file-archive text-warning';
+ default: return 'fas fa-file text-secondary';
+ }
+ },
+
+ // Viewer controls
+ openViewer(file) {
+ this.fullscreenItem = file;
+ if (this.isImage(file)) {
+ this.currentImageIndex = this.imageFiles.findIndex(img => img.id === file.id);
+ }
+ this.resetZoomAndPan();
+ this.$nextTick(() => { this.$refs.viewer?.focus(); });
+ },
+ closeViewer() {
+ this.fullscreenItem = null;
+ },
+ navigateImage(direction) {
+ const newIndex = this.currentImageIndex + direction;
+ if (newIndex >= 0 && newIndex < this.imageFiles.length) {
+ this.currentImageIndex = newIndex;
+ this.fullscreenItem = this.imageFiles[newIndex];
+ this.resetZoomAndPan();
+ }
+ },
+
+ // Event handlers for keyboard and clicks
+ handleKeyDown(event) {
+ if (!this.fullscreenItem) return;
+ switch (event.key) {
+ case 'Escape': this.closeViewer(); break;
+ case 'ArrowLeft': this.isViewingImage && this.navigateImage(-1); break;
+ case 'ArrowRight': this.isViewingImage && this.navigateImage(1); break;
+ }
+ },
+
+ // --- Zoom and Pan Methods ---
+ resetZoomAndPan() {
+ this.zoom = 1;
+ this.pan = { x: 0, y: 0 };
+ this.isPanning = false;
+ },
+
+ // Mouse Wheel Zoom
+ handleWheel(e) {
+ if (!this.isViewingImage) return;
+ e.preventDefault();
+ const scaleFactor = 0.2;
+ const newZoom = this.zoom - (e.deltaY > 0 ? scaleFactor : -scaleFactor);
+ this.zoom = Math.max(1, Math.min(newZoom, 5)); // Clamp zoom between 1x and 5x
+ },
+
+ // Mouse Drag to Pan
+ onPanStart(e) {
+ if (this.zoom <= 1) return;
+ e.preventDefault();
+ this.isPanning = true;
+ this.panStart.x = e.clientX - this.pan.x;
+ this.panStart.y = e.clientY - this.pan.y;
+ },
+ onPanMove(e) {
+ if (!this.isPanning) return;
+ this.pan.x = e.clientX - this.panStart.x;
+ this.pan.y = e.clientY - this.panStart.y;
+ },
+ onPanEnd() {
+ this.isPanning = false;
+ },
+
+ // Touch Events for Mobile (Pinch-to-Zoom & Pan)
+ onTouchStart(e) {
+ if (this.zoom <= 1 && e.touches.length === 1) return;
+ e.preventDefault();
+ if (e.touches.length === 1) { // Pan
+ this.isPanning = true;
+ this.panStart.x = e.touches[0].clientX - this.pan.x;
+ this.panStart.y = e.touches[0].clientY - this.pan.y;
+ } else if (e.touches.length === 2) { // Zoom
+ this.lastPinchDist = Math.hypot(
+ e.touches[0].clientX - e.touches[1].clientX,
+ e.touches[0].clientY - e.touches[1].clientY
+ );
+ }
+ },
+ onTouchMove(e) {
+ if (!this.isPanning && e.touches.length !== 2) return;
+ e.preventDefault();
+ if (e.touches.length === 1 && this.isPanning) { // Pan
+ this.pan.x = e.touches[0].clientX - this.panStart.x;
+ this.pan.y = e.touches[0].clientY - this.panStart.y;
+ } else if (e.touches.length === 2) { // Zoom
+ const pinchDist = Math.hypot(
+ e.touches[0].clientX - e.touches[1].clientX,
+ e.touches[0].clientY - e.touches[1].clientY
+ );
+ const scaleFactor = 0.01;
+ const newZoom = this.zoom + (pinchDist - this.lastPinchDist) * scaleFactor;
+ this.zoom = Math.max(1, Math.min(newZoom, 5)); // Clamp zoom
+ this.lastPinchDist = pinchDist;
+ }
+ },
+ onTouchEnd(e) {
+ this.isPanning = false;
+ if (e.touches.length < 2) {
+ this.lastPinchDist = 0;
+ }
+ }
+ },
+ watch: {
+ fullscreenItem(newItem) {
+ // Prevent body scroll when viewer is open
+ document.body.style.overflow = newItem ? 'hidden' : '';
+ }
+ },
+ template: `
+
+
+
Keine Dokumente vorhanden.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ file.fileName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+});
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/tt-table-crud.js b/public/plugins/vue/tt-components/tt-table-crud.js
index 8fca7e71b..07e04a9d0 100644
--- a/public/plugins/vue/tt-components/tt-table-crud.js
+++ b/public/plugins/vue/tt-components/tt-table-crud.js
@@ -181,7 +181,7 @@ Vue.component('tt-table-crud', {
key: this.crudConfig.key,
tableHeader: this.crudConfig.tableHeader,
headers: this.crudConfig.columns.filter(column => column.table !== false).map(column => {
- return {text: column.text, key: column.key, ...column.table, filterOptions: column?.modal?.items, priority: column.priority}
+ return {text: column.text, key: column.key, ...column.table, filterOptions: column?.table?.filterOptions ?? column?.modal?.items ?? [], priority: column.priority}
})
}
}, modalConfig() {
diff --git a/public/plugins/vue/tt-components/tt-table.js b/public/plugins/vue/tt-components/tt-table.js
index ecfede3fe..40ee7d451 100644
--- a/public/plugins/vue/tt-components/tt-table.js
+++ b/public/plugins/vue/tt-components/tt-table.js
@@ -201,9 +201,9 @@ Vue.component('tt-table', {
.isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key])
.format('DD.MM.YYYY HH:mm')) : ''
}}
-
+
+
+
{{ autoCompleteData[key] && autoCompleteData[key][row[key]] ? autoCompleteData[key][row[key]]['title'] : row[key] }}
@@ -236,9 +236,9 @@ Vue.component('tt-table', {
}}
{{ columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text }}
{{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}
-
+
+
+
{{ autoCompleteData[key] && autoCompleteData[key][row[key]] ? autoCompleteData[key][row[key]]['title'] : row[key] }}