204 lines
8.8 KiB
JavaScript
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);
|
|
}
|