feat(frontend): add tab navigation and table view to AdminMembershipsComponent
- Add three tabs: Ausstehende Anträge / Aktive Mitgliedschaften / Alle - Card view for pending tab (existing approve/reject behavior preserved) - Table view for active and all tabs (Name, Email, Community, Zählpunkt, Priorität, Status) - getStatusLabel() for German status display (Ausstehend/Aktiv/Inaktiv) - getStatusBadgeClass() for color-coded status badges - Client-side filtering for instant tab switching - Add comprehensive tests (19 test cases) - Add getAllUsers() to AdminIAMService (fixes compilation for admin-user-approval) - Fix deleteUserTariff missing userId argument in user-tariff component
This commit is contained in:
parent
dbd39643aa
commit
ef933ed0ea
255
eeg_frontend/src/app/api/api/adminIAM.service.ts
Normal file
255
eeg_frontend/src/app/api/api/adminIAM.service.ts
Normal file
@ -0,0 +1,255 @@
|
||||
/**
|
||||
* OpenAPI definition
|
||||
*
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
/* tslint:disable:no-unused-variable member-ordering */
|
||||
|
||||
import { Inject, Injectable, Optional } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams,
|
||||
HttpResponse, HttpEvent, HttpContext
|
||||
} from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { OpenApiHttpParams, QueryParamStyle } from '../query.params';
|
||||
|
||||
// @ts-ignore
|
||||
import { UserProfileResponse } from '../model/userProfileResponse';
|
||||
|
||||
// @ts-ignore
|
||||
import { BASE_PATH, COLLECTION_FORMATS } from '../variables';
|
||||
import { Configuration } from '../configuration';
|
||||
import { BaseService } from '../api.base.service';
|
||||
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AdminIAMService extends BaseService {
|
||||
|
||||
constructor(protected httpClient: HttpClient, @Optional() @Inject(BASE_PATH) basePath: string|string[], @Optional() configuration?: Configuration) {
|
||||
super(basePath, configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* User freigeben
|
||||
* Setzt enabled=true, status=APPROVED und triggert die finale EDA-Anmeldung.
|
||||
* @endpoint post /api/admin/users/{userId}/approve
|
||||
* @param userId
|
||||
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
|
||||
* @param reportProgress flag to report request and response progress.
|
||||
* @param options additional options
|
||||
*/
|
||||
public approveUser(userId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<any>;
|
||||
public approveUser(userId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<any>>;
|
||||
public approveUser(userId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<any>>;
|
||||
public approveUser(userId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<any> {
|
||||
if (userId === null || userId === undefined) {
|
||||
throw new Error('Required parameter userId was null or undefined when calling approveUser.');
|
||||
}
|
||||
|
||||
let localVarHeaders = this.defaultHeaders;
|
||||
|
||||
const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([
|
||||
]);
|
||||
if (localVarHttpHeaderAcceptSelected !== undefined) {
|
||||
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
|
||||
}
|
||||
|
||||
const localVarHttpContext: HttpContext = options?.context ?? new HttpContext();
|
||||
|
||||
const localVarTransferCache: boolean = options?.transferCache ?? true;
|
||||
|
||||
|
||||
let responseType_: 'text' | 'json' | 'blob' = 'json';
|
||||
if (localVarHttpHeaderAcceptSelected) {
|
||||
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
|
||||
responseType_ = 'text';
|
||||
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
|
||||
responseType_ = 'json';
|
||||
} else {
|
||||
responseType_ = 'blob';
|
||||
}
|
||||
}
|
||||
|
||||
let localVarPath = `/api/admin/users/${this.configuration.encodeParam({name: "userId", value: userId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/approve`;
|
||||
const { basePath, withCredentials } = this.configuration;
|
||||
return this.httpClient.request<any>('post', `${basePath}${localVarPath}`,
|
||||
{
|
||||
context: localVarHttpContext,
|
||||
responseType: <any>responseType_,
|
||||
...(withCredentials ? { withCredentials } : {}),
|
||||
headers: localVarHeaders,
|
||||
observe: observe,
|
||||
...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}),
|
||||
reportProgress: reportProgress
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle wartenden User
|
||||
* Zeigt User im Status PENDING inkl. der Ergebnisse des EDA-Netz-Checks.
|
||||
* @endpoint get /api/admin/users/pending
|
||||
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
|
||||
* @param reportProgress flag to report request and response progress.
|
||||
* @param options additional options
|
||||
*/
|
||||
public getPendingUsers(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<Array<UserProfileResponse>>;
|
||||
public getPendingUsers(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<Array<UserProfileResponse>>>;
|
||||
public getPendingUsers(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<Array<UserProfileResponse>>>;
|
||||
public getPendingUsers(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<any> {
|
||||
|
||||
let localVarHeaders = this.defaultHeaders;
|
||||
|
||||
const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([
|
||||
'application/json'
|
||||
]);
|
||||
if (localVarHttpHeaderAcceptSelected !== undefined) {
|
||||
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
|
||||
}
|
||||
|
||||
const localVarHttpContext: HttpContext = options?.context ?? new HttpContext();
|
||||
|
||||
const localVarTransferCache: boolean = options?.transferCache ?? true;
|
||||
|
||||
|
||||
let responseType_: 'text' | 'json' | 'blob' = 'json';
|
||||
if (localVarHttpHeaderAcceptSelected) {
|
||||
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
|
||||
responseType_ = 'text';
|
||||
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
|
||||
responseType_ = 'json';
|
||||
} else {
|
||||
responseType_ = 'blob';
|
||||
}
|
||||
}
|
||||
|
||||
let localVarPath = `/api/admin/users/pending`;
|
||||
const { basePath, withCredentials } = this.configuration;
|
||||
return this.httpClient.request<Array<UserProfileResponse>>('get', `${basePath}${localVarPath}`,
|
||||
{
|
||||
context: localVarHttpContext,
|
||||
responseType: <any>responseType_,
|
||||
...(withCredentials ? { withCredentials } : {}),
|
||||
headers: localVarHeaders,
|
||||
observe: observe,
|
||||
...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}),
|
||||
reportProgress: reportProgress
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle User
|
||||
* Zeigt alle registrierten Benutzer.
|
||||
* @endpoint get /api/admin/users
|
||||
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
|
||||
* @param reportProgress flag to report request and response progress.
|
||||
* @param options additional options
|
||||
*/
|
||||
public getAllUsers(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<Array<UserProfileResponse>>;
|
||||
public getAllUsers(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<Array<UserProfileResponse>>>;
|
||||
public getAllUsers(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<Array<UserProfileResponse>>>;
|
||||
public getAllUsers(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<any> {
|
||||
|
||||
let localVarHeaders = this.defaultHeaders;
|
||||
|
||||
const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([
|
||||
'application/json'
|
||||
]);
|
||||
if (localVarHttpHeaderAcceptSelected !== undefined) {
|
||||
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
|
||||
}
|
||||
|
||||
const localVarHttpContext: HttpContext = options?.context ?? new HttpContext();
|
||||
|
||||
const localVarTransferCache: boolean = options?.transferCache ?? true;
|
||||
|
||||
let responseType_: 'text' | 'json' | 'blob' = 'json';
|
||||
if (localVarHttpHeaderAcceptSelected) {
|
||||
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
|
||||
responseType_ = 'text';
|
||||
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
|
||||
responseType_ = 'json';
|
||||
} else {
|
||||
responseType_ = 'blob';
|
||||
}
|
||||
}
|
||||
|
||||
let localVarPath = `/api/admin/users`;
|
||||
const { basePath, withCredentials } = this.configuration;
|
||||
return this.httpClient.request<Array<UserProfileResponse>>('get', `${basePath}${localVarPath}`,
|
||||
{
|
||||
context: localVarHttpContext,
|
||||
responseType: <any>responseType_,
|
||||
...(withCredentials ? { withCredentials } : {}),
|
||||
headers: localVarHeaders,
|
||||
observe: observe,
|
||||
...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}),
|
||||
reportProgress: reportProgress
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* User ablehnen
|
||||
* Lehnt den Antrag ab (z.B. weil Zählpunkt nicht ins Netz passt).
|
||||
* @endpoint post /api/admin/users/{userId}/reject
|
||||
* @param userId
|
||||
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
|
||||
* @param reportProgress flag to report request and response progress.
|
||||
* @param options additional options
|
||||
*/
|
||||
public rejectUser(userId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<any>;
|
||||
public rejectUser(userId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<any>>;
|
||||
public rejectUser(userId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<any>>;
|
||||
public rejectUser(userId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable<any> {
|
||||
if (userId === null || userId === undefined) {
|
||||
throw new Error('Required parameter userId was null or undefined when calling rejectUser.');
|
||||
}
|
||||
|
||||
let localVarHeaders = this.defaultHeaders;
|
||||
|
||||
const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([
|
||||
]);
|
||||
if (localVarHttpHeaderAcceptSelected !== undefined) {
|
||||
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
|
||||
}
|
||||
|
||||
const localVarHttpContext: HttpContext = options?.context ?? new HttpContext();
|
||||
|
||||
const localVarTransferCache: boolean = options?.transferCache ?? true;
|
||||
|
||||
|
||||
let responseType_: 'text' | 'json' | 'blob' = 'json';
|
||||
if (localVarHttpHeaderAcceptSelected) {
|
||||
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
|
||||
responseType_ = 'text';
|
||||
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
|
||||
responseType_ = 'json';
|
||||
} else {
|
||||
responseType_ = 'blob';
|
||||
}
|
||||
}
|
||||
|
||||
let localVarPath = `/api/admin/users/${this.configuration.encodeParam({name: "userId", value: userId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}/reject`;
|
||||
const { basePath, withCredentials } = this.configuration;
|
||||
return this.httpClient.request<any>('post', `${basePath}${localVarPath}`,
|
||||
{
|
||||
context: localVarHttpContext,
|
||||
responseType: <any>responseType_,
|
||||
...(withCredentials ? { withCredentials } : {}),
|
||||
headers: localVarHeaders,
|
||||
observe: observe,
|
||||
...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}),
|
||||
reportProgress: reportProgress
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,46 +1,132 @@
|
||||
<div class="max-w-4xl mx-auto p-4 md:p-6">
|
||||
<h1 class="text-2xl font-bold mb-6">Mitgliedschaftsanträge</h1>
|
||||
<div class="max-w-6xl mx-auto p-4 md:p-6">
|
||||
<h1 class="text-2xl font-bold mb-6">Mitgliedschaften verwalten</h1>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="border-b border-gray-200 mb-6">
|
||||
<nav class="flex gap-4" role="tablist">
|
||||
<button role="tab"
|
||||
[attr.aria-selected]="activeTab() === 'pending'"
|
||||
(click)="switchTab('pending')"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors"
|
||||
[class]="activeTab() === 'pending'
|
||||
? 'border-blue-600 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'">
|
||||
Ausstehende Anträge
|
||||
</button>
|
||||
<button role="tab"
|
||||
[attr.aria-selected]="activeTab() === 'active'"
|
||||
(click)="switchTab('active')"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors"
|
||||
[class]="activeTab() === 'active'
|
||||
? 'border-blue-600 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'">
|
||||
Aktive Mitgliedschaften
|
||||
</button>
|
||||
<button role="tab"
|
||||
[attr.aria-selected]="activeTab() === 'all'"
|
||||
(click)="switchTab('all')"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors"
|
||||
[class]="activeTab() === 'all'
|
||||
? 'border-blue-600 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'">
|
||||
Alle
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mb-2"></div>
|
||||
<div>Lade Anträge...</div>
|
||||
</div>
|
||||
} @else if (pendingMemberships().length === 0) {
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6 text-center">
|
||||
<p class="text-gray-600">Keine wartenden Mitgliedschaftsanträge vorhanden.</p>
|
||||
<div>Lade Daten...</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="space-y-4">
|
||||
@for (membership of pendingMemberships(); track membership.id) {
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-semibold text-gray-900">{{ membership.firstName }} {{ membership.lastName }}</span>
|
||||
<span class="text-sm text-gray-500">({{ membership.userEmail }})</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="font-medium">Community:</span> {{ membership.communityName }} |
|
||||
<span class="font-medium">Zählpunkt:</span> {{ membership.atNumber }} |
|
||||
<span class="font-medium">Priorität:</span> {{ membership.priorityLevel }}
|
||||
<!-- Pending Tab: Card View -->
|
||||
@if (activeTab() === 'pending') {
|
||||
@if (pendingMemberships().length === 0) {
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6 text-center">
|
||||
<p class="text-gray-600">Keine wartenden Mitgliedschaftsanträge vorhanden.</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="space-y-4">
|
||||
@for (membership of pendingMemberships(); track membership.id) {
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-semibold text-gray-900">{{ membership.firstName }} {{ membership.lastName }}</span>
|
||||
<span class="text-sm text-gray-500">({{ membership.userEmail }})</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="font-medium">Community:</span> {{ membership.communityName }} |
|
||||
<span class="font-medium">Zählpunkt:</span> {{ membership.atNumber }} |
|
||||
<span class="font-medium">Priorität:</span> {{ membership.priorityLevel }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button (click)="approveMembership(membership.id)"
|
||||
[disabled]="isProcessing() === membership.id"
|
||||
class="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 disabled:opacity-50 text-sm font-medium">
|
||||
{{ isProcessing() === membership.id ? '...' : 'Genehmigen' }}
|
||||
</button>
|
||||
<button (click)="rejectMembership(membership.id)"
|
||||
[disabled]="isProcessing() === membership.id"
|
||||
class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 disabled:opacity-50 text-sm font-medium">
|
||||
{{ isProcessing() === membership.id ? '...' : 'Ablehnen' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button (click)="approveMembership(membership.id)"
|
||||
[disabled]="isProcessing() === membership.id"
|
||||
class="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 disabled:opacity-50 text-sm font-medium">
|
||||
{{ isProcessing() === membership.id ? '...' : 'Genehmigen' }}
|
||||
</button>
|
||||
<button (click)="rejectMembership(membership.id)"
|
||||
[disabled]="isProcessing() === membership.id"
|
||||
class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 disabled:opacity-50 text-sm font-medium">
|
||||
{{ isProcessing() === membership.id ? '...' : 'Ablehnen' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Active / All Tab: Table View -->
|
||||
@if (activeTab() === 'active' || activeTab() === 'all') {
|
||||
@let displayMemberships = activeTab() === 'all' ? allMemberships() : allMemberships().filter(m => m.status === 'ACTIVE');
|
||||
@if (displayMemberships.length === 0) {
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6 text-center">
|
||||
<p class="text-gray-600">
|
||||
@if (activeTab() === 'active') {
|
||||
Keine aktiven Mitgliedschaften vorhanden.
|
||||
} @else {
|
||||
Keine Mitgliedschaften vorhanden.
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Community</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Zählpunkt</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Priorität</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@for (membership of displayMemberships; track membership.id) {
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-sm text-gray-900">{{ membership.firstName }} {{ membership.lastName }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600">{{ membership.userEmail }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600">{{ membership.communityName }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600">{{ membership.atNumber }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600">{{ membership.priorityLevel }}</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
||||
[class]="getStatusBadgeClass(membership.status)">
|
||||
{{ getStatusLabel(membership.status) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,247 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { AdminMembershipsComponent } from './admin-memberships';
|
||||
import { PendingMembershipResponse } from '../../services/membership';
|
||||
|
||||
describe('AdminMembershipsComponent', () => {
|
||||
let component: AdminMembershipsComponent;
|
||||
let fixture: ComponentFixture<AdminMembershipsComponent>;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
const mockPendingMembership: PendingMembershipResponse = {
|
||||
id: '1',
|
||||
userEmail: 'max@test.at',
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
atNumber: 'AT0000000000000000001',
|
||||
communityName: 'Test Community',
|
||||
priorityLevel: 1,
|
||||
status: 'PENDING',
|
||||
};
|
||||
|
||||
const mockActiveMembership: PendingMembershipResponse = {
|
||||
id: '2',
|
||||
userEmail: 'anna@test.at',
|
||||
firstName: 'Anna',
|
||||
lastName: 'Schmidt',
|
||||
atNumber: 'AT0000000000000000002',
|
||||
communityName: 'Active Community',
|
||||
priorityLevel: 2,
|
||||
status: 'ACTIVE',
|
||||
};
|
||||
|
||||
const mockInactiveMembership: PendingMembershipResponse = {
|
||||
id: '3',
|
||||
userEmail: 'tom@test.at',
|
||||
firstName: 'Tom',
|
||||
lastName: 'Berger',
|
||||
atNumber: 'AT0000000000000000003',
|
||||
communityName: 'Inactive Community',
|
||||
priorityLevel: 3,
|
||||
status: 'INACTIVE',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AdminMembershipsComponent],
|
||||
providers: [provideHttpClient(), provideHttpClientTesting()],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminMembershipsComponent);
|
||||
component = fixture.componentInstance;
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
fixture.detectChanges();
|
||||
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
req.flush([]);
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should default activeTab to pending', () => {
|
||||
fixture.detectChanges();
|
||||
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
req.flush([]);
|
||||
expect(component.activeTab()).toBe('pending');
|
||||
});
|
||||
|
||||
it('should initialize allMemberships as empty array', () => {
|
||||
fixture.detectChanges();
|
||||
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
req.flush([]);
|
||||
expect(component.allMemberships()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should load pending memberships on init', () => {
|
||||
fixture.detectChanges();
|
||||
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
expect(req.request.method).toBe('GET');
|
||||
req.flush([mockPendingMembership]);
|
||||
expect(component.pendingMemberships().length).toBe(1);
|
||||
expect(component.pendingMemberships()[0].firstName).toBe('Max');
|
||||
});
|
||||
|
||||
it('should load all memberships when switching to active tab', () => {
|
||||
fixture.detectChanges();
|
||||
const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
pendingReq.flush([]);
|
||||
|
||||
component.switchTab('active');
|
||||
|
||||
const allReq = httpMock.expectOne(
|
||||
(r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'),
|
||||
);
|
||||
expect(allReq.request.method).toBe('GET');
|
||||
allReq.flush([mockPendingMembership, mockActiveMembership, mockInactiveMembership]);
|
||||
|
||||
expect(component.allMemberships().length).toBe(3);
|
||||
});
|
||||
|
||||
it('should load all memberships when switching to all tab', () => {
|
||||
fixture.detectChanges();
|
||||
const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
pendingReq.flush([]);
|
||||
|
||||
component.switchTab('all');
|
||||
|
||||
const allReq = httpMock.expectOne(
|
||||
(r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'),
|
||||
);
|
||||
expect(allReq.request.method).toBe('GET');
|
||||
allReq.flush([mockPendingMembership, mockActiveMembership]);
|
||||
|
||||
expect(component.allMemberships().length).toBe(2);
|
||||
});
|
||||
|
||||
it('should switch activeTab signal', () => {
|
||||
fixture.detectChanges();
|
||||
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
req.flush([]);
|
||||
|
||||
component.switchTab('active');
|
||||
const activeReq = httpMock.expectOne(
|
||||
(r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'),
|
||||
);
|
||||
activeReq.flush([]);
|
||||
expect(component.activeTab()).toBe('active');
|
||||
|
||||
component.switchTab('pending');
|
||||
expect(component.activeTab()).toBe('pending');
|
||||
|
||||
component.switchTab('all');
|
||||
const allReq = httpMock.expectOne(
|
||||
(r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'),
|
||||
);
|
||||
allReq.flush([]);
|
||||
expect(component.activeTab()).toBe('all');
|
||||
});
|
||||
|
||||
describe('getStatusLabel', () => {
|
||||
it('should return "Ausstehend" for PENDING', () => {
|
||||
expect(component.getStatusLabel('PENDING')).toBe('Ausstehend');
|
||||
});
|
||||
|
||||
it('should return "Aktiv" for ACTIVE', () => {
|
||||
expect(component.getStatusLabel('ACTIVE')).toBe('Aktiv');
|
||||
});
|
||||
|
||||
it('should return "Inaktiv" for INACTIVE', () => {
|
||||
expect(component.getStatusLabel('INACTIVE')).toBe('Inaktiv');
|
||||
});
|
||||
|
||||
it('should return the original status for unknown values', () => {
|
||||
expect(component.getStatusLabel('UNKNOWN')).toBe('UNKNOWN');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatusBadgeClass', () => {
|
||||
it('should return yellow badge for PENDING', () => {
|
||||
expect(component.getStatusBadgeClass('PENDING')).toContain('yellow');
|
||||
});
|
||||
|
||||
it('should return green badge for ACTIVE', () => {
|
||||
expect(component.getStatusBadgeClass('ACTIVE')).toContain('green');
|
||||
});
|
||||
|
||||
it('should return red badge for INACTIVE', () => {
|
||||
expect(component.getStatusBadgeClass('INACTIVE')).toContain('red');
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('should render three tabs', () => {
|
||||
fixture.detectChanges();
|
||||
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
req.flush([]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const tabs = fixture.nativeElement.querySelectorAll('[role="tab"]');
|
||||
expect(tabs.length).toBe(3);
|
||||
expect(tabs[0].textContent).toContain('Ausstehende Anträge');
|
||||
expect(tabs[1].textContent).toContain('Aktive Mitgliedschaften');
|
||||
expect(tabs[2].textContent).toContain('Alle');
|
||||
});
|
||||
|
||||
it('should show pending tab as active by default', () => {
|
||||
fixture.detectChanges();
|
||||
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
req.flush([]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const activeTab = fixture.nativeElement.querySelector('[role="tab"][aria-selected="true"]');
|
||||
expect(activeTab?.textContent).toContain('Ausstehende Anträge');
|
||||
});
|
||||
|
||||
it('should show card view for pending tab', () => {
|
||||
fixture.detectChanges();
|
||||
const req = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
req.flush([mockPendingMembership]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const cards = fixture.nativeElement.querySelectorAll('.bg-white.border');
|
||||
expect(cards.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should show table view for active tab', () => {
|
||||
fixture.detectChanges();
|
||||
const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
pendingReq.flush([]);
|
||||
|
||||
component.switchTab('active');
|
||||
const allReq = httpMock.expectOne(
|
||||
(r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'),
|
||||
);
|
||||
allReq.flush([mockActiveMembership]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const table = fixture.nativeElement.querySelector('table');
|
||||
expect(table).toBeTruthy();
|
||||
const rows = fixture.nativeElement.querySelectorAll('tbody tr');
|
||||
expect(rows.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should show table view for all tab', () => {
|
||||
fixture.detectChanges();
|
||||
const pendingReq = httpMock.expectOne((r) => r.url.includes('/admin/memberships/pending'));
|
||||
pendingReq.flush([]);
|
||||
|
||||
component.switchTab('all');
|
||||
const allReq = httpMock.expectOne(
|
||||
(r) => r.url.includes('/admin/memberships') && !r.url.includes('/pending'),
|
||||
);
|
||||
allReq.flush([mockPendingMembership, mockActiveMembership]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const table = fixture.nativeElement.querySelector('table');
|
||||
expect(table).toBeTruthy();
|
||||
const rows = fixture.nativeElement.querySelectorAll('tbody tr');
|
||||
expect(rows.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,17 +1,21 @@
|
||||
import {Component, inject, OnInit, signal} from '@angular/core';
|
||||
import {MembershipService, PendingMembershipResponse} from '../../services/membership';
|
||||
import {ToastService} from '../../services/toast';
|
||||
import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MembershipService, PendingMembershipResponse } from '../../services/membership';
|
||||
import { ToastService } from '../../services/toast';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-memberships',
|
||||
standalone: true,
|
||||
templateUrl: './admin-memberships.html'
|
||||
templateUrl: './admin-memberships.html',
|
||||
})
|
||||
export class AdminMembershipsComponent implements OnInit {
|
||||
private membershipService = inject(MembershipService);
|
||||
private toastService = inject(ToastService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
activeTab = signal<'pending' | 'active' | 'all'>('pending');
|
||||
pendingMemberships = signal<PendingMembershipResponse[]>([]);
|
||||
allMemberships = signal<PendingMembershipResponse[]>([]);
|
||||
isLoading = signal(true);
|
||||
isProcessing = signal<string | null>(null);
|
||||
|
||||
@ -19,8 +23,16 @@ export class AdminMembershipsComponent implements OnInit {
|
||||
this.loadPendingMemberships();
|
||||
}
|
||||
|
||||
switchTab(tab: 'pending' | 'active' | 'all') {
|
||||
this.activeTab.set(tab);
|
||||
if ((tab === 'active' || tab === 'all') && this.allMemberships().length === 0) {
|
||||
this.loadAllMemberships();
|
||||
}
|
||||
}
|
||||
|
||||
loadPendingMemberships() {
|
||||
this.membershipService.getPendingMemberships().subscribe({
|
||||
this.isLoading.set(true);
|
||||
this.membershipService.getPendingMemberships().pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||
next: memberships => {
|
||||
this.pendingMemberships.set(memberships);
|
||||
this.isLoading.set(false);
|
||||
@ -32,6 +44,46 @@ export class AdminMembershipsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
loadAllMemberships() {
|
||||
this.isLoading.set(true);
|
||||
this.membershipService.getAllMemberships().pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||
next: memberships => {
|
||||
this.allMemberships.set(memberships);
|
||||
this.isLoading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.isLoading.set(false);
|
||||
this.toastService.error('Fehler beim Laden aller Mitgliedschaften.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getStatusLabel(status: string): string {
|
||||
switch (status) {
|
||||
case 'PENDING':
|
||||
return 'Ausstehend';
|
||||
case 'ACTIVE':
|
||||
return 'Aktiv';
|
||||
case 'INACTIVE':
|
||||
return 'Inaktiv';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
getStatusBadgeClass(status: string): string {
|
||||
switch (status) {
|
||||
case 'PENDING':
|
||||
return 'bg-yellow-100 text-yellow-800';
|
||||
case 'ACTIVE':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 'INACTIVE':
|
||||
return 'bg-red-100 text-red-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
}
|
||||
|
||||
approveMembership(id: string) {
|
||||
if (this.isProcessing()) return;
|
||||
this.isProcessing.set(id);
|
||||
|
||||
@ -5,6 +5,7 @@ import {UserTariffControllerService, TariffControllerService, UserTariffRequest,
|
||||
import {MeteringPointService, MeteringPoint} from '../../services/metering-point';
|
||||
import {MembershipService, MembershipResponse} from '../../services/membership';
|
||||
import {ToastService} from '../../services/toast';
|
||||
import {AuthService} from '../../services/auth';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-tariff',
|
||||
@ -19,6 +20,7 @@ export class UserTariffComponent implements OnInit, OnDestroy {
|
||||
private meteringPointService = inject(MeteringPointService);
|
||||
private membershipService = inject(MembershipService);
|
||||
private toastService = inject(ToastService);
|
||||
private authService = inject(AuthService);
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
memberships = signal<MembershipResponse[]>([]);
|
||||
@ -187,7 +189,9 @@ export class UserTariffComponent implements OnInit, OnDestroy {
|
||||
onDelete(tariff: UserTariffResponse) {
|
||||
if (!confirm('Möchtest du diesen Tarif wirklich löschen?')) return;
|
||||
|
||||
this.userTariffService.deleteUserTariff(tariff.id!).subscribe({
|
||||
const userId = this.authService.getCurrentUserId();
|
||||
if (!userId) return;
|
||||
this.userTariffService.deleteUserTariff(tariff.id!, userId).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Tarif erfolgreich gelöscht.');
|
||||
this.loadUserTariffs(this.selectedCommunityId());
|
||||
|
||||
Loading…
Reference in New Issue
Block a user