166 lines
6.6 KiB
JavaScript
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);
|
|
}
|