Files
thetool/public/js/pages/Radius/RadiusOntFinder.js
2025-12-09 05:34:24 +00:00

204 lines
8.8 KiB
JavaScript

/* ===== RadiusOntFinder.js (Vue 3 + TT-Core) ===== */
const RadiusOntFinder = {
name: 'RadiusOntFinder',
template: `
<div class="tt-scope ont-card">
<div v-if="step===1">
<div class="block-head">
<div class="h4"><i class="fa-duotone fa-file-spreadsheet"></i> Schritt 1 · Excel (XLSX) Upload</div>
<p class="muted small">Datei muss die Spalte <code>Serial</code> enthalten. Optional <code>MAC</code>.</p>
</div>
<tt-file-dropzone accept=".xlsx,.xls" @file-selected="readXlsx" />
<div v-if="uploadError" class="alert error mt-2">{{ uploadError }}</div>
</div>
<div v-if="step===2">
<div class="block-head">
<div class="h4"><i class="fa-duotone fa-list-check"></i> Ergebnisse</div>
<div class="cluster">
<button class="primary-btn" @click="downloadResults" :disabled="loading">
<i class="fa-duotone fa-download"></i> Ergebnisse herunterladen
</button>
<button class="ghost-btn" @click="resetComponent" :disabled="loading">
<i class="fa-duotone fa-rotate-right"></i> Neue Datei
</button>
</div>
</div>
<div class="results-container mt-between">
<tt-loading-indicator
v-if="loading"
:text="currentSerial"
:progress="progress"
style="min-height: 200px;"
/>
<tt-data-table
v-else
:items="processedData"
:has-searched="true"
no-results-placeholder-text="Keine Daten verarbeitet."
>
<template #head>
<thead>
<tr>
<th v-for="h in originalHeaders" :key="'h'+h">{{ h }}</th>
<th>Username</th>
<th>Kundennummer</th>
<th>Kundenname</th>
<th>Info</th>
</tr>
</thead>
</template>
<template #row="{ item }">
<td v-for="h in originalHeaders" :key="h+item.Serial">{{ item[h] }}</td>
<td class="mono">{{ item.fetched_username }}</td>
<td class="mono">{{ item.fetched_customerNumber }}</td>
<td class="clamp-2">{{ item.fetched_customerName }}</td>
<td class="clamp-2 mono">{{ item.fetched_info }}</td>
</template>
</tt-data-table>
<div v-if="!loading && processedData.length" class="results-summary">
{{ processedData.length }} Zeilen verarbeitet
</div>
</div>
</div>
</div>
`,
data: () => ({
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 i = this.$el.querySelector('input[type="file"]');
if (i) i.value = '';
},
async readXlsx(file) {
this.uploadError = null;
try {
await window.TT_CORE.loadScript('https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js');
const arr = await new Promise((res, rej) => {
const r = new FileReader();
r.onload = e => res(new Uint8Array(e.target.result));
r.onerror = () => rej(new Error('Fehler beim Lesen.'));
r.readAsArrayBuffer(file);
});
const wb = XLSX.read(arr, {type: 'array'});
const ws = wb.Sheets[wb.SheetNames[0]];
this.parsedData = XLSX.utils.sheet_to_json(ws, {defval: ''});
if (!this.parsedData.length) throw new Error('Die Datei ist leer.');
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 (e) {
this.uploadError = e.message;
this.step = 1;
}
},
async startProcessing() {
this.step = 2;
this.loading = true;
this.totalRows = this.parsedData.length;
this.processedData = [];
const setRow = (row, msg, data = {}) => {
const d = {
username: `N/A - ${msg}`,
customerNumber: 'N/A',
customerName: 'N/A',
info: 'N/A'
};
Object.keys(this.fetchedKeys).forEach(k => row[this.fetchedKeys[k]] = data[k] || d[k]);
};
for (const [i, row] of this.parsedData.entries()) {
this.currentRow = i;
const out = {...row};
const sn = ('' + (row[this.serialColumnName] || '')).trim();
this.currentSerial = `SN: ${sn || '—'}`;
let found = false;
if (sn) {
try {
const { data } = await axios.get(`${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius`, {
params: { ont_sn: sn }
});
if (Array.isArray(data) && data.length > 0) {
setRow(out, '', data[0]);
found = true;
}
} catch {
}
}
if (!found && this.originalHeaders.includes(this.macColumnName)) {
const macRaw = ('' + (row[this.macColumnName] || '')).trim();
if (macRaw && macRaw.length === 12) {
const mac = macRaw.toUpperCase().match(/.{1,2}/g).join(':');
try {
const { data: ses } = await axios.get(`${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius`, {
params: { action2: 'find_by_current_session', mac }
});
if (Array.isArray(ses) && ses.length > 0) {
const { data: d } = await axios.get(`${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius`, {
params: { username: ses[0], info: '', custnum: '' }
});
if (Array.isArray(d) && d.length > 0) {
setRow(out, '', d[0]);
found = true;
}
}
} catch {
}
}
}
if (!found) setRow(out, 'Keinen Benutzer gefunden');
this.processedData.push(out);
this.progress = ((i + 1) / this.totalRows) * 100;
if ((i + 1) % 20 === 0) await new Promise(r => setTimeout(r, 20));
}
this.loading = false;
this.currentSerial = '';
},
downloadResults() {
if (!this.processedData.length) return;
try {
const data = this.processedData.map(r => {
const o = {};
this.originalHeaders.forEach(h => o[h] = r[h]);
Object.keys(this.fetchedKeys).forEach(k => {
const K = k.charAt(0).toUpperCase() + k.slice(1).replace('Number', 'nummer').replace('Name', 'name');
o[K] = r[this.fetchedKeys[k]];
});
return o;
});
const ws = XLSX.utils.json_to_sheet(data);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'ONT_Finder_Results');
XLSX.writeFile(wb, `ont_finder_results_${new Date().toISOString().replace(/[-:.]/g, '').slice(0, 14)}.xlsx`);
} catch {
if (window.notify) window.notify('error', 'Fehler beim Erstellen der Excel-Datei.');
}
}
}
};
// Register component with Vue 3 app
if (window.VueApp) {
window.VueApp.component('radius-ont-finder', RadiusOntFinder);
}