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

166 lines
6.6 KiB
JavaScript

/* ===== RadiusUnused.js (Vue 3 + TT-Core) ===== */
const RadiusUnusedUsers = {
name: 'RadiusUnusedUsers',
template: `
<div class="tt-scope">
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; margin-bottom: 12px;">
<div class="cluster">
<button
v-for="f in filters"
:key="f.id"
class="tab-btn"
:class="{active: activeFilter === f.id}"
@click="setFilter(f.id)"
:disabled="isLoading || !users.length">
<i :class="f.icon"></i> {{f.name}}
</button>
</div>
<button class="ghost-btn" @click="fetchUnusedUsers" :disabled="isLoading" style="min-width: 120px;">
<span v-if="!isLoading"><i class="fa-duotone fa-rotate-right"></i> Neu laden</span>
<span v-else class="btn-loader"></span>
</button>
</div>
<div class="results-container">
<tt-data-table
:items="visibleFilteredUsers"
:is-loading="isLoading"
:has-searched="hasSearched"
initial-placeholder-icon="fa-duotone fa-play-circle"
initial-placeholder-text="Klicken Sie auf 'Neu laden', um nach inaktiven Benutzern zu suchen."
no-results-placeholder-text="Keine Treffer für den aktuellen Filter gefunden."
>
<template #loading-placeholder>
<div class="table-placeholder">
<i class="fa-duotone fa-hourglass-half animated-hourglass" style="font-size: 36px; margin-bottom: 10px; color: var(--brand-blue);"></i>
<div>Die Abfrage läuft...</div>
<div class="muted small">Dies kann einen Moment dauern, da große Datenmengen analysiert werden.</div>
</div>
</template>
<template #head>
<thead>
<tr>
<th style="width: 130px;">Kundennummer</th>
<th style="width: 170px;">Username</th>
<th style="width: 170px;">Letzter Login</th>
<th>Info</th>
<th style="width: 100px; text-align: right;">Sessions</th>
<th style="width: 150px; text-align: right;">Dauer</th>
<th style="width: 150px; text-align: right;">Traffic</th>
</tr>
</thead>
</template>
<template #skeleton-row>
<td><tt-skeleton /></td>
<td><tt-skeleton /></td>
<td><tt-skeleton /></td>
<td><tt-skeleton /></td>
<td><tt-skeleton /></td>
<td><tt-skeleton /></td>
<td><tt-skeleton /></td>
</template>
<template #row="{ item }">
<td>
<a v-if="item.customerNumber" class="link" target="_blank"
:href="window.TT_CONFIG.BASE_PATH + '/Address?filter%5Bcustomer_number%5D=' + item.customerNumber"
data-tooltip="Kunden öffnen"
data-tooltip-align="right">{{ item.customerNumber }}</a>
</td>
<td class="nowrap">
<a class="link" target="_blank"
:href="'http://radius.xinon.at/edit_user.php?user=' + item.username"
data-tooltip="User in Radius öffnen">{{ item.username }}</a>
</td>
<td class="mono small">{{ item.lastLogin }}</td>
<td class="mono clamp-2 small">{{ item.info }}</td>
<td style="text-align: right;">{{ item.totalSessions }}</td>
<td style="text-align: right;">{{ window.TT_CORE.formatDuration(item.totalDurationSeconds) }}</td>
<td style="text-align: right;">{{ window.TT_CORE.formatBytes(item.totalTrafficBytes) }}</td>
</template>
<template #observer>
<div ref="sentinel" style="height: 1px;"></div>
</template>
</tt-data-table>
<div v-if="hasSearched && !isLoading && filteredUsers.length" class="results-summary">
{{ filteredUsers.length }} Treffer gefunden
</div>
</div>
</div>
`,
data: () => ({
users: [],
isLoading: false,
_initialized: false,
hasSearched: false,
window: window,
visibleCount: 50,
observer: null,
activeFilter: 'all',
filters: [
{id: 'all', name:'Alle', icon:'fa-duotone fa-globe'},
{id:'nat', name:'NAT*', icon:'fa-duotone fa-users'},
{id:'st', name:'ST*', icon:'fa-duotone fa-server'},
{id:'stf', name:'STF*', icon:'fa-duotone fa-id-card-clip'}
]
}),
computed: {
filteredUsers() {
return this.activeFilter === 'all'
? this.users
: this.users.filter(u => u.username && u.username.startsWith(this.activeFilter));
},
visibleFilteredUsers() {
return this.filteredUsers.slice(0, this.visibleCount);
}
},
mounted() {
this.observer = new IntersectionObserver(([e]) => {
if (e && e.isIntersecting) this.loadMore();
}, { root: this.$refs.tableWrap, threshold: 0.1 });
if (this.$refs.sentinel) this.observer.observe(this.$refs.sentinel);
},
beforeUnmount() {
if (this.observer) this.observer.disconnect();
},
updated() {
if (this.observer && this.$refs.sentinel) {
this.observer.disconnect();
this.observer.observe(this.$refs.sentinel);
}
},
methods: {
initIfNeeded() {
if (this._initialized) return;
this._initialized = true;
},
setFilter(filter) {
this.activeFilter = filter;
this.visibleCount = 50;
},
async fetchUnusedUsers() {
this.isLoading = true;
this.hasSearched = true;
this.visibleCount = 50;
this.users = [];
try {
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius`, {
params: { action2: 'reportUnused' }
});
this.users = data || [];
} catch (error) {
console.error("Failed to fetch unused users:", error);
this.users = [];
}
this.isLoading = false;
},
loadMore() {
if (this.visibleCount < this.filteredUsers.length) this.visibleCount += 50;
}
}
};
// Register component with Vue 3 app
if (window.VueApp) {
window.VueApp.component('radius-unused-users', RadiusUnusedUsers);
}