Add new components for managing free users and ONT lookup, enhance UI styling,...
This commit is contained in:
@@ -1,190 +1,36 @@
|
||||
/* ===== RadiusOntParser.js =====
|
||||
* Styling via gemeinsame ONT CSS; keine Funktionsänderungen, nur UI-Polish.
|
||||
*/
|
||||
|
||||
/* ===== RadiusOntParser.js ===== */
|
||||
Vue.component('radius-ont-parser', {
|
||||
template: `
|
||||
<div class="radius-scope ont-card">
|
||||
<div v-if="step===1" class="block">
|
||||
<div class="block-head">
|
||||
<div class="h4"><i class="fa-duotone fa-file-spreadsheet"></i> Schritt 1 · Excel (XLSX) Upload</div>
|
||||
<div class="muted small">Laden Sie eine XLSX-Datei mit Ihren Kundendaten.</div>
|
||||
</div>
|
||||
<label class="file-drop" @dragover.prevent @drop.prevent="onDrop">
|
||||
<input type="file" accept=".xlsx" @change="handleFileUpload" hidden ref="fileInput">
|
||||
<div class="file-cta">
|
||||
<i class="fa-duotone fa-cloud-arrow-up"></i>
|
||||
<div>Hierhin ziehen oder <button type="button" class="link-btn" @click="$refs.fileInput.click()">Datei auswählen</button></div>
|
||||
</div>
|
||||
</label>
|
||||
<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><div class="muted small">Laden Sie eine XLSX-Datei mit Ihren Kundendaten.</div></div>
|
||||
<radius-file-drop @file-selected="readXlsx" />
|
||||
</div>
|
||||
|
||||
<div v-if="step===2" class="block">
|
||||
<div class="block-head">
|
||||
<div class="h4"><i class="fa-duotone fa-sliders"></i> Schritt 2 · Spaltenzuordnung</div>
|
||||
</div>
|
||||
<div class="grid g-4 cols-2 cols-1@sm">
|
||||
<div class="field" v-for="(field, idx) in requiredFields" :key="idx">
|
||||
<label>{{ field.label }}</label>
|
||||
<div class="select">
|
||||
<select v-model="selectedColumns[field.key]">
|
||||
<option v-for="header in headers" :key="header" :value="header">{{ header }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cluster mt-3">
|
||||
<button class="primary-btn" @click="startProcessing"><i class="fa-duotone fa-play"></i> Verarbeitung starten</button>
|
||||
<button class="ghost-btn" @click="step = 1"><i class="fa-duotone fa-arrow-left"></i> Zurück</button>
|
||||
<div v-if="step===2">
|
||||
<div class="block-head"><div class="h4"><i class="fa-duotone fa-sliders"></i> Schritt 2 · Spaltenzuordnung</div></div>
|
||||
<div class="grid g-4 cols-2 cols-1@sm"><div class="field" v-for="field in requiredFields" :key="field.key"><label>{{ field.label }}</label><div class="select"><select v-model="selectedColumns[field.key]"><option v-for="h in headers" :key="h" :value="h">{{ h }}</option></select></div></div></div>
|
||||
<div class="cluster mt-3"><button class="primary-btn" @click="startProcessing"><i class="fa-duotone fa-play"></i> Verarbeitung starten</button><button class="ghost-btn" @click="step = 1"><i class="fa-duotone fa-arrow-left"></i> Zurück</button></div>
|
||||
</div>
|
||||
<div v-if="step===3">
|
||||
<div class="block-head"><div class="h4"><i class="fa-duotone fa-list-check"></i> Schritt 3 · Ergebnisse</div><div class="cluster"><button class="primary-btn" @click="downloadResults" :disabled="loading"><i class="fa-duotone fa-download"></i> Neue Excel herunterladen</button><button class="ghost-btn" @click="step = 2" :disabled="loading"><i class="fa-duotone fa-arrow-left"></i> Zurück</button><button class="ghost-btn" @click="resetLocal" :disabled="loading"><i class="fa-duotone fa-rotate-right"></i> Neue Verarbeitung</button></div></div>
|
||||
<div class="results-container mt-between">
|
||||
<radius-processing-indicator v-if="loading" :progress="progress" :current-row="currentRow" :total-rows="totalRows">
|
||||
<template #description><p class="muted small">Aktueller Kunde: {{ currentCustomerNumber || '—' }}</p></template>
|
||||
</radius-processing-indicator>
|
||||
<radius-table-view v-else :items="processedData" :has-searched="true" no-results-placeholder-text="Keine Daten verarbeitet.">
|
||||
<template #head><thead><tr><th v-for="h in requiredFields" :key="h.key">{{ h.label }}</th><th>ONT SN</th></tr></thead></template>
|
||||
<template #row="{ item }"><td>{{ item[selectedColumns.kundennummer] }}</td><td>{{ item[selectedColumns.anschlussstrasse] }}</td><td>{{ item[selectedColumns.anschlussplz] }}</td><td>{{ item[selectedColumns.anschlusscity] }}</td><td class="mono">{{ item.ont_sn }}</td></template>
|
||||
</radius-table-view>
|
||||
<div v-if="!loading && processedData.length" class="results-summary">{{ processedData.length }} Zeilen verarbeitet</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="step===3" class="block">
|
||||
<div class="block-head">
|
||||
<div class="h4"><i class="fa-duotone fa-list-check"></i> Schritt 3 · Ergebnisse</div>
|
||||
<div class="cluster">
|
||||
<button class="primary-btn" @click="downloadResults"><i class="fa-duotone fa-download"></i> Neue Excel herunterladen</button>
|
||||
<button class="ghost-btn" @click="step = 2"><i class="fa-duotone fa-arrow-left"></i> Zurück</button>
|
||||
<button class="ghost-btn" @click="resetLocal"><i class="fa-duotone fa-rotate-right"></i> Neue Verarbeitung</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="tt-table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<template v-for="header in requiredFields">
|
||||
<th :key="header.key">{{ header.label }}</th>
|
||||
</template>
|
||||
<th>ONT SN</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in processedData" :key="i" class="row-fade-in">
|
||||
<td>{{ row[selectedColumns.kundennummer] }}</td>
|
||||
<td>{{ row[selectedColumns.anschlussstrasse] }}</td>
|
||||
<td>{{ row[selectedColumns.anschlussplz] }}</td>
|
||||
<td>{{ row[selectedColumns.anschlusscity] }}</td>
|
||||
<td class="mono">{{ row.ont_sn }}</td>
|
||||
</tr>
|
||||
<tr v-if="processedData.length===0">
|
||||
<td colspan="5" class="muted center p-lg">Keine Daten</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="loading" class="overlay">
|
||||
<div class="ont-loading-card pop">
|
||||
<div class="spinner-lg"></div>
|
||||
<div class="h4 mt-2">Verarbeitung läuft...</div>
|
||||
<p class="muted">Ihre Daten werden Zeile für Zeile analysiert.</p>
|
||||
<div class="progress-bar is-yellow mt-3"><div class="bar" :style="{width: progress + '%'}"></div></div>
|
||||
<div class="muted small mt-2 mono">{{ currentRow + 1 }} / {{ totalRows }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</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
|
||||
};
|
||||
},
|
||||
data: () => ({ 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, currentCustomerNumber: '' }),
|
||||
methods: {
|
||||
onDrop(e){ const f = e.dataTransfer.files?.[0]; if (f) this.readXlsx(f); },
|
||||
handleFileUpload(e){ const f = e.target.files?.[0]; if (f) this.readXlsx(f); },
|
||||
async readXlsx(file){
|
||||
await this.loadXLSX();
|
||||
const fr = new FileReader();
|
||||
fr.onload = (e)=>{
|
||||
const data = new Uint8Array(e.target.result);
|
||||
const wb = XLSX.read(data, { type: 'array' });
|
||||
const ws = wb.Sheets[wb.SheetNames[0]];
|
||||
this.parsedData = XLSX.utils.sheet_to_json(ws);
|
||||
this.headers = Object.keys(this.parsedData[0] || {});
|
||||
this.step = 2;
|
||||
};
|
||||
fr.readAsArrayBuffer(file);
|
||||
},
|
||||
async loadXLSX(){
|
||||
if (window.XLSX) return;
|
||||
await new Promise((res, rej)=>{
|
||||
const s=document.createElement('script');
|
||||
s.src='https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js';
|
||||
s.onload=res; s.onerror=rej; document.head.appendChild(s);
|
||||
});
|
||||
},
|
||||
async startProcessing(){
|
||||
this.loading = true; this.totalRows = this.parsedData.length;
|
||||
const processed = [];
|
||||
|
||||
const dataToProcess = this.parsedData;
|
||||
|
||||
loop:
|
||||
for (let i=0; i<dataToProcess.length; i++){
|
||||
this.currentRow = i; this.progress = ((i + 1) / dataToProcess.length) * 100;
|
||||
const row = { ...dataToProcess[i] };
|
||||
try{
|
||||
const findUserResponse = await fetch(
|
||||
window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?custnume=' + encodeURIComponent(row[this.selectedColumns.kundennummer])
|
||||
);
|
||||
const users = await findUserResponse.json();
|
||||
|
||||
if (users.length === 0) {
|
||||
row.ont_sn = 'N/A - Kein Benutzer mit dieser Kundennummer gefunden';
|
||||
processed.push(row);
|
||||
} else if (users.length === 1) {
|
||||
const username = users[0].username;
|
||||
const r = await fetch(
|
||||
window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?skipAdditional=true&action2=fetchRadacct&username=' + encodeURIComponent(username)
|
||||
);
|
||||
const d = await r.json();
|
||||
row.ont_sn = d.ont_sn || 'N/A - Keine ONT SN gefunden';
|
||||
processed.push(row);
|
||||
} else {
|
||||
const s = row[this.selectedColumns.anschlussstrasse];
|
||||
const p = row[this.selectedColumns.anschlussplz];
|
||||
const c = row[this.selectedColumns.anschlusscity];
|
||||
for (let u of users) {
|
||||
const info = u.info || users[0].info || '';
|
||||
if (window.RadiusUtils.validateData(s, p, c, info)) {
|
||||
const r = await fetch(
|
||||
window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?skipAdditional=true&action2=fetchRadacct&username=' + encodeURIComponent(u.username)
|
||||
);
|
||||
const d = await r.json();
|
||||
row.ont_sn = d.ont_sn || 'N/A - Keine ONT SN gefunden';
|
||||
processed.push(row);
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
row.ont_sn = 'N/A - Anschluss konnte nicht zugeordnet werden';
|
||||
processed.push(row);
|
||||
}
|
||||
} catch {
|
||||
row.ont_sn = 'N/A - Fehler bei der Verarbeitung';
|
||||
processed.push(row);
|
||||
}
|
||||
if ((i + 1) % 20 === 0) await new Promise(r=>setTimeout(r,20));
|
||||
}
|
||||
this.loading=false; this.processedData = processed; 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');
|
||||
},
|
||||
async readXlsx(file){ await window.RadiusUtils.loadScript('https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js'); const fr = new FileReader(); fr.onload = (e)=>{ const wb = XLSX.read(new Uint8Array(e.target.result), { type: 'array' }); this.parsedData = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); this.headers = Object.keys(this.parsedData[0] || {}); this.step = 2; }; fr.readAsArrayBuffer(file); },
|
||||
async startProcessing(){ this.step = 3; this.loading = true; this.totalRows = this.parsedData.length; this.processedData = []; this.currentRow = 0; const p = []; const b = window.TT_CONFIG.BASE_PATH; loop: for (let i=0; i<this.parsedData.length; i++){ this.currentRow = i; this.progress = ((i + 1) / this.totalRows) * 100; const row = { ...this.parsedData[i] }; this.currentCustomerNumber = row[this.selectedColumns.kundennummer] || ''; try{ const res = await fetch(`${b}/Radius/proxyUnsecureHTTPRequestToRadius?custnume=${encodeURIComponent(row[this.selectedColumns.kundennummer])}`); const users = await res.json(); if (users.length === 0) { row.ont_sn = 'N/A - Kein Benutzer'; } else if (users.length === 1) { const r = await fetch(`${b}/Radius/proxyUnsecureHTTPRequestToRadius?skipAdditional=true&action2=fetchRadacct&username=${encodeURIComponent(users[0].username)}`); const d = await r.json(); row.ont_sn = d.ont_sn || 'N/A - Keine ONT SN'; } else { const [s,pl,c] = [row[this.selectedColumns.anschlussstrasse], row[this.selectedColumns.anschlussplz], row[this.selectedColumns.anschlusscity]]; for (let u of users) { if (window.RadiusUtils.validateData(s, pl, c, u.info || users[0].info || '')) { const r = await fetch(`${b}/Radius/proxyUnsecureHTTPRequestToRadius?skipAdditional=true&action2=fetchRadacct&username=${encodeURIComponent(u.username)}`); const d = await r.json(); row.ont_sn = d.ont_sn || 'N/A - Keine ONT SN'; p.push(row); continue loop; } } row.ont_sn = 'N/A - Anschluss nicht zugeordnet'; } } catch { row.ont_sn = 'N/A - Fehler'; } p.push(row); if ((i + 1) % 20 === 0) await new Promise(r=>setTimeout(r,20)); } this.loading=false; this.processedData = p; },
|
||||
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'); },
|
||||
resetLocal(){ Object.assign(this.$data, this.$options.data.call(this)); }
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user