Files
thetool/public/js/pages/Radius/Radius.js
2025-07-28 15:31:50 +02:00

825 lines
35 KiB
JavaScript

// Function to calculate similarity percentage between two strings
function calculateSimilarity(str1, str2) {
// Normalize strings by converting them to lowercase
str1 = str1.toLowerCase();
str2 = str2.toLowerCase();
let matchCount = 0;
// Check how many characters in str1 exist in str2
for (let char of str1) {
if (str2.includes(char)) {
matchCount++;
}
}
// Calculate similarity percentage
return (matchCount / str1.length) * 100;
}
function validateData(strasse, plz, stadt, info) {
const thresholds = 90; // Similarity threshold in percentage
// Validate each field against the info string
return !(calculateSimilarity(strasse, info) < thresholds ||
calculateSimilarity(plz, info) < thresholds ||
calculateSimilarity(stadt, info) < thresholds);
}
Vue.component('radius-ont-parser', {
template: `
<div class="container mt-4">
<!-- Step 1: File Upload -->
<div v-if="step === 1" class="card">
<div class="card-body">
<h4 class="card-title">Schritt 1: Excel (XLSX) Upload</h4>
<input type="file" class="form-control" @change="handleFileUpload" accept=".xlsx">
</div>
</div>
<!-- Step 2: Column Mapping -->
<div v-if="step === 2" class="card mt-4">
<div class="card-body">
<h4 class="card-title">Schritt 2: Spaltenzuordnung</h4>
<div class="row">
<div class="col-md-6" v-for="(field, index) in requiredFields" :key="index">
<div class="form-group">
<label>{{ field.label }}</label>
<select class="form-control" v-model="selectedColumns[field.key]">
<option v-for="header in headers" :value="header">{{ header }}</option>
</select>
</div>
</div>
</div>
<button class="btn btn-primary mt-3" @click="startProcessing">Start Processing</button>
</div>
</div>
<!-- Step 3: Result Download -->
<div v-if="step === 3" class="card mt-4">
<div class="card-body">
<h4 class="card-title">Schritt 3: Ergebnisse</h4>
<tt-button icon="fas fa-download" text="Neue Excel herunterladen" additional-class="btn-primary" @click="downloadResults"/>
<table class="table mt-4">
<thead>
<tr>
<template v-for="header in requiredFields">
<th>{{ header.label }}</th>
</template>
<th>ONT SN</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in processedData" :key="index">
<td>{{ row[selectedColumns.kundennummer] }}</td>
<td>{{ row[selectedColumns.anschlussstrasse] }}</td>
<td>{{ row[selectedColumns.anschlussplz] }}</td>
<td>{{ row[selectedColumns.anschlusscity] }}</td>
<td>{{ row.ont_sn }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Loading Screen -->
<div v-if="loading" class="loading-overlay mt-4">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated"
:style="{ width: progress + '%' }">
{{ Math.round(progress) }}%
</div>
</div>
<div class="text-center mt-2">Processing {{ currentRow + 1 }} of {{ totalRows }}</div>
</div>
</div>
`,
data() {
return {
step: 1,
headers: [],
parsedData: [],
processedData: [],
selectedColumns: {
kundennummer: 'crmPartner',
anschlussstrasse: 'AnlStrasse',
anschlussplz: 'AnlPlz',
anschlusscity: 'AnlOrt'
},
requiredFields: [
{ key: 'kundennummer', label: 'Kundennummer' },
{ key: 'anschlussstrasse', label: 'Anschlussstraße' },
{ key: 'anschlussplz', label: 'Anschluss PLZ' },
{ key: 'anschlusscity', label: 'Anschluss City' }
],
loading: false,
progress: 0,
currentRow: 0,
totalRows: 0
};
},
methods: {
async handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Load XLSX library dynamically
await this.loadXLSX();
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: "array" });
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
// Read entire sheet as rows of arrays (no header detection yet)
const allRows = XLSX.utils.sheet_to_json(worksheet, {
header: 1, // Return rows as arrays
blankrows: false // Skip blank rows
});
// If there's only one row or something unexpected, do a basic parse
if (allRows.length < 2) {
const fallbackData = XLSX.utils.sheet_to_json(worksheet);
this.parsedData = fallbackData;
this.headers = Object.keys(fallbackData[0] || {});
this.step = 2;
return;
}
const firstRow = allRows[0] || [];
const secondRow = allRows[1] || [];
// Count how many cells in each row are empty
const firstRowEmptyCount = firstRow.length - firstRow.filter(Boolean).length;
const secondRowEmptyCount = secondRow.length - secondRow.filter(Boolean).length;
// If the difference in empty cells is more than 25% of the number of columns in the second row,
// assume the first row is mostly empty and use the second row as the header
const useSecondRowAsHeader = (firstRowEmptyCount - secondRowEmptyCount) > 0.25 * secondRow.length;
// Now parse again with the correct header row
if (useSecondRowAsHeader) {
this.parsedData = XLSX.utils.sheet_to_json(worksheet, {
range: 1, // Start reading data after the first row
header: secondRow, // Use the second row as the header
defval: "" // Optional: fill empty cells with empty string
}).slice(1); // Skip the first row
this.headers = secondRow;
} else {
this.parsedData = XLSX.utils.sheet_to_json(worksheet, {
range: 0, // Start at the first row
header: firstRow, // Use the first row as the header
defval: ""
});
this.headers = firstRow;
}
this.step = 2;
};
reader.readAsArrayBuffer(file);
},
async loadXLSX() {
if (!window.XLSX) {
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';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
},
async startProcessing() {
this.loading = true;
this.totalRows = this.parsedData.length;
const processedRows = [];
mainLoop:
for (let i = 0; i < this.parsedData.length; i++) {
this.currentRow = i;
this.progress = ((i + 1) / this.parsedData.length) * 100;
// Simulate processing
await this.sleep(100);
// Process row here
const row = this.parsedData[i];
const findUserResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?custnume=' + row[this.selectedColumns.kundennummer]);
const findUserData = await findUserResponse.json();
if (findUserData.length === 0) {
row.ont_sn = 'N/A - Kein Benutzer mit dieser Kundennummer gefunden';
processedRows.push(row);
} else if (findUserData.length === 1) {
const username = findUserData[0].username;
const radacctResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?skipAdditional=true&action2=fetchRadacct&username=' + username);
const radacctData = await radacctResponse.json();
row.ont_sn = radacctData.ont_sn || 'N/A - Keine ONT SN gefunden';
processedRows.push(row);
} else if (findUserData.length > 1) {
// check string simulairty of strasse, plz, stadt and atleast of 90% of each should be inside findUserData[].info
// if not, ont_sn = N/A - Anschluss konnte nicht zugeordnet werden
const strasse = row[this.selectedColumns.anschlussstrasse];
const plz = row[this.selectedColumns.anschlussplz];
const stadt = row[this.selectedColumns.anschlusscity];
const info = findUserData[0].info;
for (let user of findUserData) {
if (validateData(strasse, plz, stadt, info)) {
const username = user.username;
const radacctResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?skipAdditional=true&action2=fetchRadacct&username=' + username);
const radacctData = await radacctResponse.json();
row.ont_sn = radacctData.ont_sn || 'N/A - Keine ONT SN gefunden';
processedRows.push(row);
continue mainLoop;
}
}
row.ont_sn = 'N/A - Anschluss konnte nicht zugeordnet werden';
processedRows.push(row);
}
}
this.loading = false;
this.processedData = processedRows;
this.step = 3;
},
downloadResults() {
const ws = XLSX.utils.json_to_sheet(this.processedData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Results");
XLSX.writeFile(wb, "results.xlsx");
},
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
});
Vue.component('radius-online-state', {
props: ['username'],
template: `
<div ref="container">
<template v-if="data === null && observed">
<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-if="data !== null">
<div :style="'border-radius: 10px;width: 110px;text-align: center;height:22px;background-color: ' + (data.online ? 'rgba(161,253,96,0.7)' : 'rgba(253,96,96,0.7)')">
<span>{{ data.ip }}</span>
</div>
</template>
</div>
`,
data: () => ({
data: null,
observer: null,
observed: false
}),
mounted() {
this.observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && !this.observed) {
this.observed = true;
this.fetchOnlineState();
}
}, { threshold: 0.1 });
this.observer.observe(this.$refs.container);
},
beforeDestroy() {
this.observer?.disconnect();
},
methods: {
async fetchOnlineState() {
const response = await fetch(`${window.TT_CONFIG['BASE_PATH']}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${this.username}`);
if (response.ok) this.data = await response.json();
}
}
})
Vue.component('radius', {
template: `
<tt-card>
<template v-slot:header>
<h3>Radius</h3>
</template>
<div class="radius-view-selector">
<tt-button icon="fas fa-user" additional-class="btn-primary" text="Radius Benutzer" @click="view = 'radius'"/>
<tt-button icon="fas fa-user-plus" additional-class="btn-primary" text="Freie Radius Benutzer" @click="view = 'free'"/>
<tt-button text="Radius ONT Parser" icon="fas fa-cogs" additional-class="btn-primary" @click="view = 'ont'" v-show="window['TT_CONFIG']['CAN_BILLING'] === '1'"/>
<tt-button text="Radius ONT Reverse Parser" icon="fas fa-cogs" additional-class="btn-primary" @click="view = 'ontReverse'" v-show="window['TT_CONFIG']['CAN_BILLING'] === '1'"/>
</div>
<div v-if="view === 'radius'">
<div class="filters" @keyup.enter="loadRadiusUsers">
<tt-autocomplete sm :api-url="billAddrAutoCompleteUrl" label="Rechnungsadresse" v-model="custnum" ref="billAddr"/>
<tt-input sm label="Username" name="username" v-model="username"/>
<tt-input sm label="Info" name="info" v-model="info"/>
<tt-checkbox v-model="checkOnlineState" label="Online-Status abfragen" sm/>
<tt-button sm icon="fas fa-search" text="Suchen" additional-class="btn-primary" style="justify-self:center" @click="loadRadiusUsers"
:disabled="isLoading"/>
</div>
<table class="table">
<thead>
<tr>
<th>Kundennummer</th>
<th>Username</th>
<th>Info</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<tr v-for="user in radiusUsers" :key="user.id">
<td>
<a target="_blank" :href="window['TT_CONFIG']['BASE_PATH'] + '/Address?filter%5Bcustomer_number%5D=' + user.customerNumber">
{{ user.customerNumber }}
</a>
</td>
<td>
<a target="_blank" :href="'http://radius.xinon.at/edit_user.php?user=' + user.username">
{{ user.username }}
</a>
</td>
<td>{{ user.info }}</td>
<td>
<template v-if="checkOnlineState === 1">
<radius-online-state :username="user.username" :key="user.username + '_online_state_' + this.searchCount"/>
</template>
</td>
<td>
<tt-button sm icon="fas fa-sync" text="Details" @click="fetchRadacctData(user.username)" additional-class="btn-primary"/>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="view === 'free'" class="free-users-container">
<div class="free-users-column">
<h4>Freie NAT Benutzer ({{ freeNatUsers.length }})</h4>
<table class="table">
<thead>
<tr>
<th>Username</th>
<th>Info</th>
</tr>
</thead>
<tbody>
<tr v-for="user in freeNatUsers" :key="user.id">
<td><a target="_blank" :href="'http://radius.xinon.at/edit_user.php?user=' + user.username">{{ user.username }}</a></td>
<td>{{ user.info }}</td>
</tr>
</tbody>
</table>
</div>
<div class="free-users-column">
<h4>Freie STF Benutzer ({{ freeStfUsers.length }})</h4>
<table class="table">
<thead>
<tr>
<th>Username</th>
<th>Info</th>
</tr>
</thead>
<tbody>
<tr v-for="user in freeStfUsers" :key="user.id">
<td><a target="_blank" :href="'http://radius.xinon.at/edit_user.php?user=' + user.username">{{ user.username }}</a></td>
<td>{{ user.info }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="view === 'ont'">
<radius-ont-parser/>
</div>
<div v-if="view === 'ontReverse'">
<radius-ont-finder/>
</div>
<tt-modal :show.sync="showRadacctModal" title="Radacct Data" :save="false" :delete="false">
<div v-if="radacctData">
<table class="table">
<tr>
<th>Status:</th>
<td><span :class="['status-dot', radacctData.online ? 'online' : 'offline']"></span> {{ radacctData.online ? 'Online' : 'Offline' }}</td>
</tr>
<tr>
<th>IP:</th>
<td>{{ radacctData.ip }}</td>
</tr>
<tr>
<th>Username:</th>
<td><a target="_blank" :href="'http://radius.xinon.at/edit_user.php?user=' + radacctData.username">{{ radacctData.username }}</a></td>
</tr>
<tr>
<th>Customer Number:</th>
<td>{{ radacctData.customerNumber }}</td>
</tr>
<tr>
<th>Customer Name:</th>
<td>{{ radacctData.customerName }}</td>
</tr>
<tr>
<th>Info:</th>
<td>{{ radacctData.info }}</td>
</tr>
<tr>
<th>WLAN Password:</th>
<td>{{ radacctData.wlanPassword }}</td>
</tr>
<tr>
<th>Bandbreite:</th>
<td>{{ radacctData.actualBandwidth }}</td>
</tr>
</table>
</div>
</tt-modal>
</tt-card>
`,
data() {
return {
view: 'radius',
billAddrAutoCompleteUrl: window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress&fibu_primary_account=1',
radiusUsers: [],
freeNatUsers: [],
freeStfUsers: [],
username: '',
info: '',
custnum: '',
window: window,
showRadacctModal: false,
checkOnlineState: 0,
radacctData: null,
isLoading: false,
searchCount: 0,
}
},
async mounted() {
console.log("hallo");
await this.loadFreeUsers();
},
methods: {
hideRadacctModal() {
this.showRadacctModal = false;
},
async loadRadiusUsers() {
this.isLoading = true;
this.radiusUsers = [];
let custnum = '';
if (this.$refs.billAddr.displayValue.length > 5) {
custnum = this.$refs.billAddr.displayValue.match(/\[(\d+)]/)[1];
}
const params = new URLSearchParams({
username: this.username,
info: this.info,
custnum: custnum,
});
const response = await fetch(`${window.TT_CONFIG['BASE_PATH']}/Radius/proxyUnsecureHTTPRequestToRadius?${params.toString()}`);
if (response.ok) {
const users = await response.json()
if (users.length < 6) {
this.checkOnlineState = 1;
}
this.radiusUsers = users;
} else {
console.error('Failed to load radius users');
}
this.isLoading = false;
this.searchCount = this.searchCount + 1;
},
async fetchRadacctData(username) {
const params = new URLSearchParams({
action2: 'fetchRadacct',
username: username,
});
const response = await fetch(`${window.TT_CONFIG['BASE_PATH']}/Radius/proxyUnsecureHTTPRequestToRadius?${params.toString()}`);
if (response.ok) {
this.radacctData = await response.json();
this.showRadacctModal = true;
} else {
console.error('Failed to fetch radacct data');
}
},
async loadFreeUsers() {
try {
const natResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?action2=free_user&filter=nat');
const stfResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?action2=free_user&filter=stf');
if (natResponse.ok && stfResponse.ok) {
const natData = await natResponse.json();
const stfData = await stfResponse.json();
this.freeNatUsers = natData.users;
this.freeStfUsers = stfData.users;
} else {
console.error('Failed to load free users');
}
} catch (error) {
console.error('Error loading free users:', error);
}
},
},
});
Vue.component('radius-ont-finder', {
template: `
<div class="container mt-4">
<div v-if="step === 1" class="card">
<div class="card-body">
<h4 class="card-title">Schritt 1: Excel (XLSX) Upload</h4>
<p>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.</p>
<input type="file" class="form-control" @change="handleFileUpload" accept=".xlsx">
<div v-if="uploadError" class="alert alert-danger mt-3">{{ uploadError }}</div>
</div>
</div>
<div v-if="step === 2" class="card mt-4">
<div class="card-body">
<h4 class="card-title">Schritt 2: Ergebnisse</h4>
<button class="btn btn-primary mb-3" @click="downloadResults">
<i class="fas fa-download mr-2"></i>Ergebnisse herunterladen
</button>
<button class="btn btn-secondary mb-3 ml-2" @click="resetComponent">
<i class="fas fa-redo mr-2"></i>Neue Datei hochladen
</button>
<div class="table-responsive">
<table class="table table-striped table-bordered table-sm">
<thead class="thead-light">
<tr>
<th v-for="header in originalHeaders" :key="'orig-' + header">{{ header }}</th>
<th>Username</th>
<th>Kundennummer</th>
<th>Kundenname</th>
<th>Info</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in processedData" :key="index">
<td v-for="header in originalHeaders" :key="'data-' + header + '-' + index">{{ row[header] }}</td>
<td>{{ row.fetched_username }}</td>
<td>{{ row.fetched_customerNumber }}</td>
<td>{{ row.fetched_customerName }}</td>
<td>{{ row.fetched_info }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div v-if="loading" class="loading-overlay mt-4">
<div class="d-flex justify-content-center align-items-center flex-column">
<div class="spinner-border text-primary mb-3" role="status">
<span class="sr-only">Loading...</span>
</div>
<div class="progress w-75">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" :style="{ width: progress + '%' }"
:aria-valuenow="progress" aria-valuemin="0" aria-valuemax="100">
{{ Math.round(progress) }}%
</div>
</div>
<div class="text-center mt-2">Verarbeite Zeile {{ currentRow + 1 }} von {{ totalRows }}</div>
<div v-if="currentSerial" class="text-center text-muted small mt-1">Aktuelle Suche: {{ currentSerial }}</div>
</div>
</div>
</div>
`,
data() {
return {
step: 1,
parsedData: [],
processedData: [],
originalHeaders: [],
loading: false,
progress: 0,
currentRow: 0,
totalRows: 0,
currentSerial: '',
uploadError: null,
serialColumnName: 'Serial',
macColumnName: 'MAC',
fetchedKeys: {
username: 'fetched_username',
customerNumber: 'fetched_customerNumber',
customerName: 'fetched_customerName',
info: 'fetched_info'
},
apiBasePath: window.TT_CONFIG?.BASE_PATH
};
},
methods: {
resetComponent() {
Object.assign(this.$data, this.$options.data.call(this));
const input = this.$el.querySelector('input[type="file"]');
if (input) input.value = '';
},
async handleFileUpload(event) {
const file = event.target.files[0];
this.uploadError = null;
if (!file) return;
this.loading = true;
try {
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 workbook = XLSX.read(data, { type: 'array' });
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
this.parsedData = XLSX.utils.sheet_to_json(worksheet, { defval: "" });
if (!this.parsedData.length) {
throw new Error("Die hochgeladene Datei ist leer oder konnte nicht gelesen werden.");
}
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;
}
},
async loadXLSX() {
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);
});
},
async startProcessing() {
this.loading = true;
this.totalRows = this.parsedData.length;
this.processedData = [];
this.progress = 0;
this.currentRow = 0;
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=`;
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 newRow = { ...row };
const serialNumber = row[this.serialColumnName]?.trim();
this.currentSerial = `SN: ${serialNumber || 'Leer'}`;
if (!serialNumber) {
setRowStatus(newRow, 'Leere Seriennummer');
this.processedData.push(newRow);
this.progress = ((i + 1) / this.totalRows) * 100;
continue;
}
let found = false;
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(`Fetch error for SN ${serialNumber}:`, error);
}
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;
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;
this.currentSerial = '';
},
downloadResults() {
if (!this.processedData.length) return;
try {
const dataToExport = this.processedData.map(row => {
const exportRow = {};
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];
exportRow['Info'] = row[this.fetchedKeys.info];
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");
const timestamp = new Date().toISOString().replace(/[-:.]/g, "").slice(0, 14);
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.");
}
},
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
},
mounted() {
if (!window.TT_CONFIG?.BASE_PATH) {
console.warn(`Global TT_CONFIG.BASE_PATH not found. API calls will use fallback path: ${this.apiBasePath}`);
}
}
});