add admin interface, angular migration

This commit is contained in:
_Bastler
2025-11-09 01:58:54 +01:00
parent ff94ca05ce
commit 1acaf07825
100 changed files with 7129 additions and 50 deletions
+45 -1
View File
@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard, AuthUpdateGuard, AuthenticatedGuard, AnonymousGuard } from './auth/auth.guard';
import { AuthGuard, AuthUpdateGuard, AuthenticatedGuard, AnonymousGuard, AdminGuard } from './auth/auth.guard';
import { MainComponent } from './ui/main/main.component';
import { FormLoginComponent } from './pages/form-login/form-login.component';
import { FormLogin2FAComponent } from './pages/form-login-2fa/form-login-2fa.component';
@@ -35,6 +35,26 @@ import { InviteCodeComponent } from './pages/invites/code/code.component';
import { JukeboxComponent } from './pages/jukebox/jukebox.compontent';
import { FormLoginOidcComponent } from './pages/form-login-oidc/form-login-oidc.component';
import { DyndnsComponent } from './pages/account/dyndns/dyndns.component';
import { AdminComponent } from './pages/admin/admin.component';
import { AdminUsersComponent } from './pages/admin/users/users.component';
import { AdminServicesComponent } from './pages/admin/services/services.component';
import { AdminPermissionsComponent } from './pages/admin/permissions/permissions.component';
import { AdminQuotasComponent } from './pages/admin/quotas/quotas.component';
import { AdminJitsiRoomsComponent } from './pages/admin/jitsi-rooms/jitsi-rooms.component';
import { AdminMinetestAccountsComponent } from './pages/admin/minetest-accounts/minetest-accounts.component';
import { AdminShortenedUrlsComponent } from './pages/admin/shortened-urls/shortened-urls.component';
import { AdminJukeboxComponent } from './pages/admin/jukebox/jukebox.component';
import { AdminParteyMapsComponent } from './pages/admin/partey-maps/partey-maps.component';
import { AdminParteyTagsComponent } from './pages/admin/partey-tags/partey-tags.component';
import { AdminParteyReportsComponent } from './pages/admin/partey-reports/partey-reports.component';
import { AdminTimeslotsComponent } from './pages/admin/timeslots/timeslots.component';
import { AdminSystemPropertiesComponent } from './pages/admin/system-properties/system-properties.component';
import { AdminPermissionMappingsComponent } from './pages/admin/permission-mappings/permission-mappings.component';
import { AdminQuotaMappingsComponent } from './pages/admin/quota-mappings/quota-mappings.component';
import { AdminVoucherMappingsComponent } from './pages/admin/voucher-mappings/voucher-mappings.component';
import { AdminSystemProfileFieldsComponent } from './pages/admin/system-profile-fields/system-profile-fields.component';
import { AdminUserAliasesComponent } from './pages/admin/user-aliases/user-aliases.component';
import { AdminOidcClientsComponent } from './pages/admin/oidc-clients/oidc-clients.component';
const routes: Routes = [
{ path: 'profile/:username', component: UserComponent, canActivate: [AuthUpdateGuard] },
@@ -84,6 +104,30 @@ const routes: Routes = [
{ path: 'urlshortener/:code', component: UrlShortenerPasswordComponent, canActivate: [AuthUpdateGuard] },
{ path: 'invites/:quota', component: InvitesComponent, canActivate: [AuthenticatedGuard] },
{ path: 'invite/:code', component: InviteCodeComponent, canActivate: [AuthGuard] },
{
path: 'admin', component: AdminComponent, canActivate: [AdminGuard], children: [
{ path: '', redirectTo: "/admin/users", pathMatch: 'full' },
{ path: 'users', component: AdminUsersComponent, canActivate: [AdminGuard] },
{ path: 'permissions', component: AdminPermissionsComponent, canActivate: [AdminGuard] },
{ path: 'permission-mappings', component: AdminPermissionMappingsComponent, canActivate: [AdminGuard] },
{ path: 'quotas', component: AdminQuotasComponent, canActivate: [AdminGuard] },
{ path: 'quota-mappings', component: AdminQuotaMappingsComponent, canActivate: [AdminGuard] },
{ path: 'voucher-mappings', component: AdminVoucherMappingsComponent, canActivate: [AdminGuard] },
{ path: 'services', component: AdminServicesComponent, canActivate: [AdminGuard] },
{ path: 'system-properties', component: AdminSystemPropertiesComponent, canActivate: [AdminGuard] },
{ path: 'system-profile-fields', component: AdminSystemProfileFieldsComponent, canActivate: [AdminGuard] },
{ path: 'user-aliases', component: AdminUserAliasesComponent, canActivate: [AdminGuard] },
{ path: 'oidc-clients', component: AdminOidcClientsComponent, canActivate: [AdminGuard] },
{ path: 'jitsi-rooms', component: AdminJitsiRoomsComponent, canActivate: [AdminGuard] },
{ path: 'minetest-accounts', component: AdminMinetestAccountsComponent, canActivate: [AdminGuard] },
{ path: 'shortened-urls', component: AdminShortenedUrlsComponent, canActivate: [AdminGuard] },
{ path: 'jukebox', component: AdminJukeboxComponent, canActivate: [AdminGuard] },
{ path: 'partey-maps', component: AdminParteyMapsComponent, canActivate: [AdminGuard] },
{ path: 'partey-tags', component: AdminParteyTagsComponent, canActivate: [AdminGuard] },
{ path: 'partey-reports', component: AdminParteyReportsComponent, canActivate: [AdminGuard] },
{ path: 'timeslots', component: AdminTimeslotsComponent, canActivate: [AdminGuard] }
]
},
{ path: 'unavailable', component: UnavailableComponent },
{ path: 'p/:username', component: UserComponent, canActivate: [AuthUpdateGuard] },
{ path: '**', component: NotfoundComponent, pathMatch: 'full', canActivate: [AuthUpdateGuard] },]
+45 -1
View File
@@ -69,6 +69,39 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
import { MomentDateModule } from '@angular/material-moment-adapter';
import { MAT_DATE_FORMATS } from '@angular/material/core';
import { DatetimepickerComponent } from './ui/datetimepicker/datetimepicker.component';
import { AdminComponent } from './pages/admin/admin.component';
import { AdminUsersComponent } from './pages/admin/users/users.component';
import { AdminUserEditDialog } from './pages/admin/users/user.edit';
import { AdminPermissionEditDialog } from './pages/admin/permissions/permission.edit';
import { AdminQuotaEditDialog } from './pages/admin/quotas/quota.edit';
import { AdminServiceEditDialog } from './pages/admin/services/service.edit';
import { AdminPermissionMappingEditDialog } from './pages/admin/permission-mappings/permission-mapping.edit';
import { AdminQuotaMappingEditDialog } from './pages/admin/quota-mappings/quota-mapping.edit';
import { AdminServicesComponent } from './pages/admin/services/services.component';
import { AdminPermissionsComponent } from './pages/admin/permissions/permissions.component';
import { AdminQuotasComponent } from './pages/admin/quotas/quotas.component';
import { AdminJitsiRoomsComponent } from './pages/admin/jitsi-rooms/jitsi-rooms.component';
import { AdminMinetestAccountsComponent } from './pages/admin/minetest-accounts/minetest-accounts.component';
import { AdminShortenedUrlsComponent } from './pages/admin/shortened-urls/shortened-urls.component';
import { AdminShortenedUrlEditDialog } from './pages/admin/shortened-urls/shortened-url.edit';
import { AdminJukeboxComponent } from './pages/admin/jukebox/jukebox.component';
import { AdminParteyMapsComponent } from './pages/admin/partey-maps/partey-maps.component';
import { AdminParteyTagsComponent } from './pages/admin/partey-tags/partey-tags.component';
import { AdminParteyReportsComponent } from './pages/admin/partey-reports/partey-reports.component';
import { AdminTimeslotsComponent } from './pages/admin/timeslots/timeslots.component';
import { AdminSystemPropertiesComponent } from './pages/admin/system-properties/system-properties.component';
import { AdminPermissionMappingsComponent } from './pages/admin/permission-mappings/permission-mappings.component';
import { AdminQuotaMappingsComponent } from './pages/admin/quota-mappings/quota-mappings.component';
import { AdminVoucherMappingsComponent } from './pages/admin/voucher-mappings/voucher-mappings.component';
import { AdminSystemProfileFieldsComponent } from './pages/admin/system-profile-fields/system-profile-fields.component';
import { AdminUserAliasesComponent } from './pages/admin/user-aliases/user-aliases.component';
import { AdminOidcClientsComponent } from './pages/admin/oidc-clients/oidc-clients.component';
import { AdminOidcClientEditDialog } from './pages/admin/oidc-clients/oidc-client.edit';
import { AdminVoucherMappingEditDialog } from './pages/admin/voucher-mappings/voucher-mapping.edit';
import { AdminSystemPropertyEditDialog } from './pages/admin/system-properties/system-property.edit';
import { AdminSystemProfileFieldEditDialog } from './pages/admin/system-profile-fields/system-profile-field.edit';
import { AdminUserAliasEditDialog } from './pages/admin/user-aliases/user-alias.edit';
import { AdminJitsiRoomEditDialog } from './pages/admin/jitsi-rooms/jitsi-room.edit';
export function init_app(i18n: I18nService) {
@@ -132,7 +165,18 @@ export class XhrInterceptor implements HttpInterceptor {
DividerComponent, DividertestComponent,
DatetimepickerComponent,
DurationpickerComponent,
JukeboxComponent
JukeboxComponent,
AdminComponent, AdminUsersComponent, AdminUserEditDialog, AdminPermissionEditDialog, AdminQuotaEditDialog,
AdminServiceEditDialog, AdminPermissionMappingEditDialog, AdminQuotaMappingEditDialog,
AdminOidcClientEditDialog, AdminVoucherMappingEditDialog, AdminSystemPropertyEditDialog,
AdminSystemProfileFieldEditDialog, AdminUserAliasEditDialog, AdminJitsiRoomEditDialog,
AdminServicesComponent, AdminPermissionsComponent,
AdminQuotasComponent, AdminJitsiRoomsComponent, AdminMinetestAccountsComponent,
AdminShortenedUrlsComponent, AdminShortenedUrlEditDialog, AdminJukeboxComponent, AdminParteyMapsComponent,
AdminParteyTagsComponent, AdminParteyReportsComponent, AdminTimeslotsComponent,
AdminSystemPropertiesComponent, AdminPermissionMappingsComponent, AdminQuotaMappingsComponent,
AdminVoucherMappingsComponent, AdminSystemProfileFieldsComponent, AdminUserAliasesComponent,
AdminOidcClientsComponent
],
imports: [
BrowserModule,
+26
View File
@@ -105,3 +105,29 @@ export class AnonymousGuard implements CanActivate {
}
}
@Injectable({
providedIn: 'root'
})
export class AdminGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) { }
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const that = this;
return this.authService.getAuth().then((data: any) => {
if (!data.authenticated) {
return that.router.navigateByUrl(that.router.parseUrl('/login?target=' + encodeURIComponent(state.url)), { skipLocationChange: true, replaceUrl: true });
}
// Check if user has ROLE_ADMIN
if (data.authorities && data.authorities.some((auth: any) => auth.authority === 'ROLE_ADMIN')) {
return true;
}
// User is authenticated but not an admin
return that.router.navigateByUrl('/account/info');
}).catch(function (error) {
return that.router.navigateByUrl(that.router.parseUrl('/unavailable?target=' + encodeURIComponent(state.url)), { skipLocationChange: true });
});
}
}
+25 -25
View File
@@ -1,35 +1,35 @@
<h2>{{'greet' | i18n:auth.name}} <mat-icon>sentiment_satisfied_alt</mat-icon>
</h2>
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
<a mat-tab-link routerLink="info" routerLinkActive #rlainfo="routerLinkActive" [active]="rlainfo.isActive">{{'info'
| i18n}}</a>
<a mat-tab-link routerLink="profile" routerLinkActive #rlaprofile="routerLinkActive"
[active]="rlaprofile.isActive">{{'profile' | i18n}}</a>
<a mat-tab-link routerLink="security" routerLinkActive #rlasecurity="routerLinkActive"
[active]="rlasecurity.isActive">{{'security' | i18n}}</a>
<a mat-tab-link routerLink="voucher" routerLinkActive #rlavoucher="routerLinkActive"
[active]="rlavoucher.isActive">{{'vouchers' | i18n}}</a>
@if (advancedView) {
<a mat-tab-link routerLink="aliases" #rlaaliases="routerLinkActive" routerLinkActive
[active]="rlaaliases.isActive">{{'user.aliases' | i18n}}</a>
}
@if (advancedView) {
<a mat-tab-link routerLink="domains" #rladomains="routerLinkActive" routerLinkActive
[active]="rladomains.isActive">{{'user.domains' | i18n}}</a>
}
@if (advancedView) {
<a mat-tab-link routerLink="dyndns" #rladyndns="routerLinkActive" routerLinkActive
[active]="rladyndns.isActive">{{'user.dyndns' | i18n}}</a>
}
<a style="align-self: center;">
<mat-slide-toggle [(ngModel)]="advancedView">
<div class="tab-container">
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
<a mat-tab-link routerLink="info" routerLinkActive #rlainfo="routerLinkActive" [active]="rlainfo.isActive">{{'info'
| i18n}}</a>
<a mat-tab-link routerLink="profile" routerLinkActive #rlaprofile="routerLinkActive"
[active]="rlaprofile.isActive">{{'profile' | i18n}}</a>
<a mat-tab-link routerLink="security" routerLinkActive #rlasecurity="routerLinkActive"
[active]="rlasecurity.isActive">{{'security' | i18n}}</a>
<a mat-tab-link routerLink="voucher" routerLinkActive #rlavoucher="routerLinkActive"
[active]="rlavoucher.isActive">{{'vouchers' | i18n}}</a>
@if (advancedView) {
<a mat-tab-link routerLink="aliases" routerLinkActive #rlaaliases="routerLinkActive"
[active]="rlaaliases.isActive">{{'user.aliases' | i18n}}</a>
<a mat-tab-link routerLink="domains" routerLinkActive #rladomains="routerLinkActive"
[active]="rladomains.isActive">{{'user.domains' | i18n}}</a>
<a mat-tab-link routerLink="dyndns" routerLinkActive #rladyndns="routerLinkActive"
[active]="rladyndns.isActive">{{'user.dyndns' | i18n}}</a>
}
</nav>
<div class="tab-controls">
<mat-slide-toggle [checked]="advancedView" (change)="toggleAdvancedView()">
@if (!advancedView) {
<span>{{'account.advanced' | i18n}}</span>
}
</mat-slide-toggle>
</a>
</nav>
</div>
</div>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
<router-outlet></router-outlet>
@@ -0,0 +1,15 @@
.tab-container {
display: flex;
align-items: center;
gap: 16px;
nav[mat-tab-nav-bar] {
flex: 1;
}
.tab-controls {
flex-shrink: 0;
display: flex;
align-items: center;
}
}
@@ -31,4 +31,8 @@ export class AccountComponent implements OnInit {
this.advancedView = this.advancedViews.indexOf(this.router.url.replace("/account/", "")) != -1;
}
toggleAdvancedView(): void {
this.advancedView = !this.advancedView;
}
}
+74
View File
@@ -0,0 +1,74 @@
<h2>{{'admin.title' | i18n}} <mat-icon>admin_panel_settings</mat-icon></h2>
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
<a mat-tab-link routerLink="users" routerLinkActive #rlausers="routerLinkActive" [active]="rlausers.isActive">
{{'admin.users' | i18n}}
</a>
<a mat-tab-link routerLink="permission-mappings" routerLinkActive #rlapermmap="routerLinkActive"
[active]="rlapermmap.isActive">
{{'admin.permission_mappings' | i18n}}
</a>
<a mat-tab-link routerLink="quota-mappings" routerLinkActive #rlaquotamap="routerLinkActive"
[active]="rlaquotamap.isActive">
{{'admin.quota_mappings' | i18n}}
</a>
<a mat-tab-link routerLink="services" routerLinkActive #rlaservices="routerLinkActive"
[active]="rlaservices.isActive">
{{'admin.services' | i18n}}
</a>
<a mat-tab-link routerLink="oidc-clients" routerLinkActive #rlaoidc="routerLinkActive" [active]="rlaoidc.isActive">
{{'admin.oidc_clients' | i18n}}
</a>
<a mat-tab-link routerLink="voucher-mappings" routerLinkActive #rlavouchers="routerLinkActive"
[active]="rlavouchers.isActive">
{{'admin.voucher_mappings' | i18n}}
</a>
<a mat-tab-link routerLink="system-properties" routerLinkActive #rlasystem="routerLinkActive"
[active]="rlasystem.isActive">
{{'admin.system_properties' | i18n}}
</a>
<a mat-tab-link routerLink="system-profile-fields" routerLinkActive #rlaprofile="routerLinkActive"
[active]="rlaprofile.isActive">
{{'admin.system_profile_fields' | i18n}}
</a>
<a mat-tab-link routerLink="user-aliases" routerLinkActive #rlaaliases="routerLinkActive"
[active]="rlaaliases.isActive">
{{'admin.user_aliases' | i18n}}
</a>
<a mat-tab-link routerLink="jitsi-rooms" routerLinkActive #rlajitsi="routerLinkActive" [active]="rlajitsi.isActive">
{{'admin.jitsi_rooms' | i18n}}
</a>
<a mat-tab-link routerLink="shortened-urls" routerLinkActive #rlaurls="routerLinkActive"
[active]="rlaurls.isActive">
{{'admin.shortened_urls' | i18n}}
</a>
<!--
<a mat-tab-link routerLink="minetest-accounts" routerLinkActive #rlaminetest="routerLinkActive"
[active]="rlaminetest.isActive">
{{'admin.minetest_accounts' | i18n}}
</a>
<a mat-tab-link routerLink="jukebox" routerLinkActive #rlajukebox="routerLinkActive"
[active]="rlajukebox.isActive">
{{'admin.jukebox' | i18n}}
</a>
<a mat-tab-link routerLink="partey-maps" routerLinkActive #rlamaps="routerLinkActive"
[active]="rlamaps.isActive">
{{'admin.partey_maps' | i18n}}
</a>
<a mat-tab-link routerLink="partey-tags" routerLinkActive #rlatags="routerLinkActive"
[active]="rlatags.isActive">
{{'admin.partey_tags' | i18n}}
</a>
<a mat-tab-link routerLink="partey-reports" routerLinkActive #rlareports="routerLinkActive"
[active]="rlareports.isActive">
{{'admin.partey_reports' | i18n}}
</a>
<a mat-tab-link routerLink="timeslots" routerLinkActive #rlatimeslots="routerLinkActive"
[active]="rlatimeslots.isActive">
{{'admin.timeslots' | i18n}}
</a>
-->
</nav>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
<router-outlet></router-outlet>
+29
View File
@@ -0,0 +1,29 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatTabNavPanel } from '@angular/material/tabs';
import { Router } from '@angular/router';
import { AuthService } from './../../services/auth.service';
@Component({
standalone: false,
selector: 'app-admin',
templateUrl: './admin.component.html',
styleUrls: ['./admin.component.scss']
})
export class AdminComponent implements OnInit {
auth;
@ViewChild('tabPanel') tabPanel: MatTabNavPanel;
constructor(private authService: AuthService, private router: Router) {
this.authService.auth.subscribe({
next: (data) => {
this.auth = data;
}
})
}
ngOnInit(): void {
}
}
+35
View File
@@ -0,0 +1,35 @@
/* Common styles for all admin components */
header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.spacer {
flex: 1 1 auto;
}
table {
width: 100%;
}
mat-card {
margin-bottom: 20px;
}
/* Common styles for all admin edit dialogs */
mat-dialog-content form {
padding-top: 20px;
}
mat-form-field {
width: 100%;
margin-bottom: 16px;
}
mat-checkbox {
display: block;
margin-bottom: 16px;
}
@@ -0,0 +1,49 @@
<h2 mat-dialog-title>{{ (isEditMode ? 'admin.jitsi_rooms.edit' : 'admin.jitsi_rooms.create') | i18n }}</h2>
<mat-dialog-content>
<form [formGroup]="form">
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.jitsi_rooms.owner' | i18n }}</mat-label>
<input matInput type="number" formControlName="owner" [placeholder]="'admin.jitsi_rooms.owner' | i18n">
<mat-hint>{{ 'admin.jitsi_rooms.owner_hint' | i18n }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.jitsi_rooms.room' | i18n }}</mat-label>
<input matInput formControlName="room" [placeholder]="'admin.jitsi_rooms.room' | i18n" required>
@if (form.get('room')?.hasError('required')) {
<mat-error>
{{ 'admin.jitsi_rooms.room_required' | i18n }}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.jitsi_rooms.starts' | i18n }}</mat-label>
<input matInput [matDatepicker]="startsPicker" formControlName="starts" [placeholder]="'admin.jitsi_rooms.starts' | i18n">
<mat-datepicker-toggle matSuffix [for]="startsPicker"></mat-datepicker-toggle>
<mat-datepicker #startsPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.jitsi_rooms.moderation_starts' | i18n }}</mat-label>
<input matInput [matDatepicker]="moderationStartsPicker" formControlName="moderationStarts" [placeholder]="'admin.jitsi_rooms.moderation_starts' | i18n">
<mat-datepicker-toggle matSuffix [for]="moderationStartsPicker"></mat-datepicker-toggle>
<mat-datepicker #moderationStartsPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.jitsi_rooms.expires' | i18n }}</mat-label>
<input matInput [matDatepicker]="expiresPicker" formControlName="expires" [placeholder]="'admin.jitsi_rooms.expires' | i18n">
<mat-datepicker-toggle matSuffix [for]="expiresPicker"></mat-datepicker-toggle>
<mat-datepicker #expiresPicker></mat-datepicker>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">{{ 'admin.cancel' | i18n }}</button>
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!form.valid">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,65 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { JitsiRoomManagementService } from '../../../services/admin/jitsiroom.management.service';
@Component({
standalone: false,
selector: 'admin-jitsi-room-edit-dialog',
templateUrl: './jitsi-room.edit.html',
styleUrls: ['../admin.scss']
})
export class AdminJitsiRoomEditDialog implements OnInit {
form: FormGroup;
isEditMode: boolean = false;
constructor(
private fb: FormBuilder,
private jitsiRoomManagementService: JitsiRoomManagementService,
public dialogRef: MatDialogRef<AdminJitsiRoomEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.isEditMode = !!data?.jitsiRoom;
// Convert ISO strings to Date objects for the datepicker
const startsDate = data?.jitsiRoom?.starts ? new Date(data.jitsiRoom.starts) : null;
const moderationStartsDate = data?.jitsiRoom?.moderationStarts ? new Date(data.jitsiRoom.moderationStarts) : null;
const expiresDate = data?.jitsiRoom?.expires ? new Date(data.jitsiRoom.expires) : null;
this.form = this.fb.group({
id: [data?.jitsiRoom?.id || null],
owner: [data?.jitsiRoom?.owner || null],
room: [data?.jitsiRoom?.room || '', Validators.required],
starts: [startsDate],
moderationStarts: [moderationStartsDate],
expires: [expiresDate]
});
}
ngOnInit(): void {}
onSubmit(): void {
if (this.form.valid) {
const jitsiRoom = {
...this.form.value,
// Convert Date objects back to ISO strings
starts: this.form.value.starts ? this.form.value.starts.toISOString() : null,
moderationStarts: this.form.value.moderationStarts ? this.form.value.moderationStarts.toISOString() : null,
expires: this.form.value.expires ? this.form.value.expires.toISOString() : null
};
this.jitsiRoomManagementService.createOrUpdateJitsiRoom(jitsiRoom).subscribe({
next: (result) => {
this.dialogRef.close(result);
},
error: (error) => {
console.error('Error saving jitsi room:', error);
}
});
}
}
onCancel(): void {
this.dialogRef.close();
}
}
@@ -0,0 +1,66 @@
<header>
<h3>{{'admin.jitsi_rooms' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary" (click)="createRoom()">
<mat-icon>add</mat-icon>
{{'admin.jitsi_rooms.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> {{'admin.jitsi_rooms.id' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.id}} </td>
</ng-container>
<ng-container matColumnDef="owner">
<th mat-header-cell *matHeaderCellDef> {{'admin.jitsi_rooms.owner' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.owner}} </td>
</ng-container>
<ng-container matColumnDef="room">
<th mat-header-cell *matHeaderCellDef> {{'admin.jitsi_rooms.room' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.room}} </td>
</ng-container>
<ng-container matColumnDef="starts">
<th mat-header-cell *matHeaderCellDef> {{'admin.jitsi_rooms.starts' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.starts | date:'short'}} </td>
</ng-container>
<ng-container matColumnDef="moderationStarts">
<th mat-header-cell *matHeaderCellDef> {{'admin.jitsi_rooms.moderation_starts' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.moderationStarts | date:'short'}} </td>
</ng-container>
<ng-container matColumnDef="expires">
<th mat-header-cell *matHeaderCellDef> {{'admin.jitsi_rooms.expires' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.expires | date:'short'}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let room">
<button mat-icon-button (click)="editRoom(room)" [title]="'admin.jitsi_rooms.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteJitsiRoom(room.id)" [title]="'admin.jitsi_rooms.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,105 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from './../../../services/i18n.service';
import { JitsiRoomManagementService } from '../../../services/admin/jitsiroom.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
import { AdminJitsiRoomEditDialog } from './jitsi-room.edit';
@Component({
standalone: false,
selector: 'app-admin-jitsi-rooms',
templateUrl: './jitsi-rooms.component.html',
styleUrls: ['../admin.scss']
})
export class AdminJitsiRoomsComponent implements OnInit {
displayedColumns: string[] = ['id', 'owner', 'room', 'starts', 'moderationStarts', 'expires', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
@ViewChild(MatPaginator) paginator: MatPaginator;
constructor(
private jitsiRoomManagementService: JitsiRoomManagementService,
private i18n: I18nService,
public dialog: MatDialog
) {
this.dataSource = new MatTableDataSource<any>([]);
}
ngOnInit(): void {
this.loadJitsiRooms();
}
loadJitsiRooms(): void {
this.jitsiRoomManagementService.getJitsiRooms(this.pageIndex, this.pageSize)
.subscribe({
next: (data: any) => {
this.dataSource.data = data.content;
this.totalElements = data.totalElements;
},
error: (error) => {
console.error('Error loading Jitsi rooms:', error);
}
});
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadJitsiRooms();
}
deleteJitsiRoom(id: number, quota: boolean = false): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.jitsi_rooms.confirm_delete',
'args': [id.toString()]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.jitsiRoomManagementService.deleteJitsiRoom(id, quota).subscribe({
next: () => {
this.loadJitsiRooms();
},
error: (error) => {
console.error('Error deleting Jitsi room:', error);
}
});
}
});
}
createRoom(): void {
const dialogRef = this.dialog.open(AdminJitsiRoomEditDialog, {
width: '600px',
data: {}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadJitsiRooms();
}
});
}
editRoom(room: any): void {
const dialogRef = this.dialog.open(AdminJitsiRoomEditDialog, {
width: '600px',
data: { jitsiRoom: room }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadJitsiRooms();
}
});
}
}
@@ -0,0 +1,53 @@
<header>
<h3>{{'admin.jukebox' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button [color]="config?.active ? 'warn' : 'accent'" (click)="toggleActive()">
<mat-icon>{{ config?.active ? 'stop' : 'play_arrow' }}</mat-icon>
{{ (config?.active ? 'admin.jukebox.disable' : 'admin.jukebox.activate') | i18n }}
</button>
</header>
@if (loading) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
}
<div style="margin-top: 20px;">
<h4>{{'admin.jukebox.configuration' | i18n}}</h4>
<form [formGroup]="configForm" (ngSubmit)="saveConfig()">
<mat-form-field>
<mat-label>{{'admin.jukebox.channel' | i18n}}</mat-label>
<input matInput formControlName="channel" required>
<mat-error>{{'admin.jukebox.error.channel' | i18n}}</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'admin.jukebox.max_queue_size' | i18n}}</mat-label>
<input matInput type="number" formControlName="maxQueueSize" required min="1">
<mat-error>{{'admin.jukebox.error.max_queue_size' | i18n}}</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'admin.jukebox.max_search_results' | i18n}}</mat-label>
<input matInput type="number" formControlName="maxSearchResults" required min="1">
<mat-error>{{'admin.jukebox.error.max_search_results' | i18n}}</mat-error>
</mat-form-field>
<mat-checkbox formControlName="autoplay">
{{'admin.jukebox.autoplay' | i18n}}
</mat-checkbox>
<div style="margin-top: 20px;">
<button mat-raised-button color="primary" type="submit" [disabled]="configForm.invalid || loading">
<mat-icon>save</mat-icon>
{{'admin.jukebox.save_config' | i18n}}
</button>
</div>
</form>
</div>
@if (status) {
<div style="margin-top: 30px;">
<h4>{{'admin.jukebox.status' | i18n}}</h4>
<pre>{{ status | json }}</pre>
</div>
}
@@ -0,0 +1,116 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from './../../../services/i18n.service';
import { JukeboxManagementService } from '../../../services/admin/jukebox.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
@Component({
standalone: false,
selector: 'app-admin-jukebox',
templateUrl: './jukebox.component.html',
styleUrls: ['../admin.scss']
})
export class AdminJukeboxComponent implements OnInit {
configForm: FormGroup;
config: any = null;
status: any = null;
loading: boolean = false;
constructor(
private jukeboxManagementService: JukeboxManagementService,
private i18n: I18nService,
private formBuilder: FormBuilder,
public dialog: MatDialog
) {
this.configForm = this.formBuilder.group({
active: [false],
autoplay: [false],
channel: ['', Validators.required],
maxQueueSize: [10, [Validators.required, Validators.min(1)]],
maxSearchResults: [10, [Validators.required, Validators.min(1)]]
});
}
ngOnInit(): void {
this.loadConfig();
this.loadStatus();
}
loadConfig(): void {
this.loading = true;
this.jukeboxManagementService.getConfig().subscribe({
next: (data: any) => {
this.config = data;
this.configForm.patchValue(data);
this.loading = false;
},
error: (error) => {
console.error('Error loading jukebox config:', error);
this.loading = false;
}
});
}
loadStatus(): void {
this.jukeboxManagementService.getStatus().subscribe({
next: (data: any) => {
this.status = data;
},
error: (error) => {
console.error('Error loading jukebox status:', error);
}
});
}
saveConfig(): void {
if (this.configForm.invalid) {
return;
}
this.loading = true;
this.jukeboxManagementService.setConfig(this.configForm.value).subscribe({
next: (data: any) => {
this.config = data;
this.loading = false;
},
error: (error) => {
console.error('Error saving jukebox config:', error);
this.loading = false;
}
});
}
toggleActive(): void {
if (this.config?.active) {
this.disable();
} else {
this.activate();
}
}
activate(): void {
this.jukeboxManagementService.setActive().subscribe({
next: () => {
this.loadConfig();
this.loadStatus();
},
error: (error) => {
console.error('Error activating jukebox:', error);
}
});
}
disable(): void {
this.jukeboxManagementService.disable().subscribe({
next: () => {
this.loadConfig();
this.loadStatus();
},
error: (error) => {
console.error('Error disabling jukebox:', error);
}
});
}
}
@@ -0,0 +1,51 @@
<header>
<h3>{{'admin.minetest_accounts' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary">
<mat-icon>add</mat-icon>
{{'admin.minetest_accounts.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{'admin.minetest_accounts.name' | i18n}} </th>
<td mat-cell *matCellDef="let account"> {{account.name}} </td>
</ng-container>
<ng-container matColumnDef="owner">
<th mat-header-cell *matHeaderCellDef> {{'admin.minetest_accounts.owner' | i18n}} </th>
<td mat-cell *matCellDef="let account"> {{account.owner}} </td>
</ng-container>
<ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef> {{'admin.minetest_accounts.created' | i18n}} </th>
<td mat-cell *matCellDef="let account"> {{account.created | date:'short'}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let account">
<button mat-icon-button [title]="'admin.minetest_accounts.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteMinetestAccount(account.name)" [title]="'admin.minetest_accounts.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,78 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from './../../../services/i18n.service';
import { MinetestAccountManagementService } from '../../../services/admin/minetestaccount.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
@Component({
standalone: false,
selector: 'app-admin-minetest-accounts',
templateUrl: './minetest-accounts.component.html',
styleUrls: ['../admin.scss']
})
export class AdminMinetestAccountsComponent implements OnInit {
displayedColumns: string[] = ['name', 'owner', 'created', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
@ViewChild(MatPaginator) paginator: MatPaginator;
constructor(
private minetestAccountManagementService: MinetestAccountManagementService,
private i18n: I18nService,
public dialog: MatDialog
) {
this.dataSource = new MatTableDataSource<any>([]);
}
ngOnInit(): void {
this.loadMinetestAccounts();
}
loadMinetestAccounts(): void {
this.minetestAccountManagementService.getMinetestAccounts(this.pageIndex, this.pageSize)
.subscribe({
next: (data: any) => {
this.dataSource.data = data.content;
this.totalElements = data.totalElements;
},
error: (error) => {
console.error('Error loading Minetest accounts:', error);
}
});
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadMinetestAccounts();
}
deleteMinetestAccount(name: string, quota: boolean = false): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.minetest_accounts.confirm_delete',
'args': [name]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.minetestAccountManagementService.deleteMinetestAccount(name, quota).subscribe({
next: () => {
this.loadMinetestAccounts();
},
error: (error) => {
console.error('Error deleting Minetest account:', error);
}
});
}
});
}
}
@@ -0,0 +1,153 @@
<h2 mat-dialog-title>{{ isEditMode ? ('admin.oidc_clients.edit_client' | i18n) : ('admin.oidc_clients.create_client' |
i18n) }}</h2>
<mat-dialog-content>
<form [formGroup]="form">
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.client_name' | i18n}}</mat-label>
<input matInput formControlName="clientName" required>
@if (form.get('clientName')?.hasError('required')) {
<mat-error>
{{'admin.oidc_clients.client_name_required' | i18n}}
</mat-error>
}
</mat-form-field>
@if (isEditMode) {
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.client_id' | i18n}}</mat-label>
<input matInput formControlName="clientId" readonly>
<button mat-icon-button matSuffix (click)="copyToClipboard(form.get('clientId')?.value, 'admin.oidc_clients.client_id_copied')" type="button" [title]="'admin.oidc_clients.copy_client_id' | i18n">
<mat-icon>content_copy</mat-icon>
</button>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.client_secret' | i18n}}</mat-label>
<input matInput [type]="hideSecret ? 'password' : 'text'" formControlName="clientSecret" readonly>
<button mat-icon-button matSuffix (click)="hideSecret = !hideSecret" type="button" [title]="hideSecret ? ('admin.oidc_clients.show_secret' | i18n) : ('admin.oidc_clients.hide_secret' | i18n)">
<mat-icon>{{hideSecret ? 'visibility' : 'visibility_off'}}</mat-icon>
</button>
<button mat-icon-button matSuffix (click)="copyToClipboard(form.get('clientSecret')?.value, 'admin.oidc_clients.secret_copied')" type="button" [title]="'admin.oidc_clients.copy_secret' | i18n">
<mat-icon>content_copy</mat-icon>
</button>
</mat-form-field>
}
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.redirect_uris' | i18n}}</mat-label>
<textarea matInput formControlName="redirectUris" rows="3" required></textarea>
<mat-hint>{{'admin.oidc_clients.redirect_uris_hint' | i18n}}</mat-hint>
@if (form.get('redirectUris')?.hasError('required')) {
<mat-error>
{{'admin.oidc_clients.redirect_uris_required' | i18n}}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.auth_methods' | i18n}}</mat-label>
<mat-select formControlName="clientAuthenticationMethods" multiple>
@for (method of availableAuthMethods; track method) {
<mat-option [value]="method">
{{method}}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.grant_types' | i18n}}</mat-label>
<mat-select formControlName="authorizationGrantTypes" multiple>
@for (type of availableGrantTypes; track type) {
<mat-option [value]="type">
{{type}}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.scopes' | i18n}}</mat-label>
<mat-select formControlName="scopes" multiple>
@for (scope of availableScopes; track scope) {
<mat-option [value]="scope">
{{scope}}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.login_url' | i18n}}</mat-label>
<input matInput formControlName="loginUrl">
</mat-form-field>
@if (isEditMode) {
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.token_lifetime' | i18n}}</mat-label>
<input matInput type="number" formControlName="tokenLifetime">
<mat-hint>{{'admin.oidc_clients.token_lifetime_hint' | i18n}}</mat-hint>
</mat-form-field>
}
@if (isEditMode) {
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.category' | i18n}}</mat-label>
<input matInput formControlName="category">
</mat-form-field>
}
<!-- Logout URIs section -->
@if (isEditMode) {
<div>
<h4>{{'admin.oidc_clients.logout_settings' | i18n}}</h4>
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.frontchannel_logout_uri' | i18n}}</mat-label>
<input matInput formControlName="frontchannelLogoutUri">
</mat-form-field>
<mat-checkbox formControlName="frontchannelLogoutSessionRequired" class="checkbox-field">
{{'admin.oidc_clients.frontchannel_logout_session_required' | i18n}}
</mat-checkbox>
<mat-form-field appearance="outline">
<mat-label>{{'admin.oidc_clients.backchannel_logout_uri' | i18n}}</mat-label>
<input matInput formControlName="backchannelLogoutUri">
</mat-form-field>
<mat-checkbox formControlName="backchannelLogoutSessionRequired" class="checkbox-field">
{{'admin.oidc_clients.backchannel_logout_session_required' | i18n}}
</mat-checkbox>
</div>
}
<!-- Advanced settings -->
@if (isEditMode) {
<div>
<h4>{{'admin.oidc_clients.advanced_settings' | i18n}}</h4>
<mat-checkbox formControlName="authorize" class="checkbox-field">
{{'admin.oidc_clients.authorize' | i18n}}
</mat-checkbox>
<mat-checkbox formControlName="aliasAllowed" class="checkbox-field">
{{'admin.oidc_clients.alias_allowed' | i18n}}
</mat-checkbox>
<mat-checkbox formControlName="aliasQuota" class="checkbox-field">
{{'admin.oidc_clients.alias_quota' | i18n}}
</mat-checkbox>
<mat-checkbox formControlName="aliasSubject" class="checkbox-field">
{{'admin.oidc_clients.alias_subject' | i18n}}
</mat-checkbox>
</div>
}
<mat-checkbox formControlName="alwaysPermitted" class="checkbox-field">
{{'admin.oidc_clients.always_permitted' | i18n}}
</mat-checkbox>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="cancel()">{{'admin.cancel' | i18n}}</button>
<button mat-raised-button color="primary" (click)="onSave()" [disabled]="!form.valid">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,20 @@
/* OIDC Client specific styles */
.checkbox-field {
display: block;
margin: 12px 0;
}
mat-dialog-content {
min-width: 500px;
max-height: 70vh;
overflow-y: auto;
}
h4 {
margin-top: 20px;
margin-bottom: 10px;
color: rgba(0, 0, 0, 0.6);
font-size: 14px;
font-weight: 500;
}
@@ -0,0 +1,144 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { OidcClientManagementService } from 'src/app/services/admin/oidcclient.management.service';
import { I18nService } from 'src/app/services/i18n.service';
@Component({
standalone: false,
selector: 'app-admin-oidc-client-edit',
templateUrl: './oidc-client.edit.html',
styleUrls: ['../admin.scss', './oidc-client.edit.scss']
})
export class AdminOidcClientEditDialog implements OnInit {
form: FormGroup;
isEditMode: boolean = false;
hideSecret: boolean = true;
// Available authentication methods
availableAuthMethods = ['basic', 'post', 'none'];
// Available grant types
availableGrantTypes = ['authorization_code', 'refresh_token', 'client_credentials'];
// Available scopes
availableScopes = ['openid', 'profile', 'email', 'offline_access'];
constructor(
private fb: FormBuilder,
private oidcClientService: OidcClientManagementService,
private dialogRef: MatDialogRef<AdminOidcClientEditDialog>,
private i18n: I18nService,
private snackBar: MatSnackBar,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.isEditMode = !!data?.client;
this.form = this.fb.group({
id: [data?.client?.id || null],
clientName: [{ value: data?.client?.clientName || '', disabled: this.isEditMode }, Validators.required],
clientId: [{ value: data?.client?.clientId || '', disabled: true }],
clientSecret: [{ value: data?.client?.clientSecret || '', disabled: true }],
redirectUris: [this.formatArrayToString(data?.client?.redirectUris), Validators.required],
clientAuthenticationMethods: [data?.client?.clientAuthenticationMethods || ['basic', 'post']],
authorizationGrantTypes: [data?.client?.authorizationGrantTypes || ['authorization_code']],
scopes: [data?.client?.scopes || ['openid', 'profile', 'email']],
loginUrl: [data?.client?.loginUrl || ''],
tokenLifetime: [data?.client?.tokenLifetime || 3600],
frontchannelLogoutUri: [data?.client?.frontchannelLogoutUri || ''],
frontchannelLogoutSessionRequired: [data?.client?.frontchannelLogoutSessionRequired || false],
backchannelLogoutUri: [data?.client?.backchannelLogoutUri || ''],
backchannelLogoutSessionRequired: [data?.client?.backchannelLogoutSessionRequired || false],
authorize: [data?.client?.authorize || false],
aliasAllowed: [data?.client?.aliasAllowed || false],
aliasQuota: [data?.client?.aliasQuota || false],
aliasSubject: [data?.client?.aliasSubject !== undefined ? data.client.aliasSubject : true],
alwaysPermitted: [data?.client?.alwaysPermitted || false],
category: [data?.client?.category || '']
});
}
ngOnInit(): void { }
/**
* Format array to comma-separated string
*/
private formatArrayToString(arr: any[]): string {
if (!arr || arr.length === 0) return '';
return Array.from(arr).join(', ');
}
/**
* Parse comma-separated string to array
*/
private parseStringToArray(str: string): string[] {
if (!str || str.trim() === '') return [];
return str.split(',').map(item => item.trim()).filter(item => item.length > 0);
}
onSave(): void {
if (this.form.valid) {
const formValue = this.form.getRawValue();
// Parse comma-separated redirectUris into array
const redirectUris = this.parseStringToArray(formValue.redirectUris);
if (this.isEditMode) {
// For update, send full OidcClient object
const client = {
...formValue,
redirectUris: redirectUris
};
this.oidcClientService.update(client).subscribe({
next: (result) => {
this.dialogRef.close(result);
},
error: (error) => {
console.error('Error updating OIDC client:', error);
}
});
} else {
// For create, use OidcClientModel format
const clientModel = {
name: formValue.clientName,
registeredRedirectUris: redirectUris,
clientAuthenticationMethods: formValue.clientAuthenticationMethods,
authorizationGrantTypes: formValue.authorizationGrantTypes,
scopes: formValue.scopes,
loginUrl: formValue.loginUrl,
alwaysPermitted: formValue.alwaysPermitted
};
this.oidcClientService.create(clientModel).subscribe({
next: (result) => {
this.dialogRef.close(result);
},
error: (error) => {
console.error('Error creating OIDC client:', error);
}
});
}
}
}
cancel(): void {
this.dialogRef.close();
}
copyToClipboard(text: string | null, messageKey: string): void {
if (text) {
navigator.clipboard.writeText(text).then(
() => {
this.snackBar.open(this.i18n.get(messageKey, []), this.i18n.get("close", []), {
duration: 3000
});
},
(err) => {
console.error('Failed to copy to clipboard:', err);
}
);
}
}
}
@@ -0,0 +1,53 @@
<header>
<h3>{{'admin.oidc_clients' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary" (click)="createClient()">
<mat-icon>add</mat-icon>
{{'admin.oidc_clients.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.oidc_clients.id' | i18n}}</th>
<td mat-cell *matCellDef="let client">{{client.id}}</td>
</ng-container>
<ng-container matColumnDef="clientId">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.oidc_clients.client_id' | i18n}}</th>
<td mat-cell *matCellDef="let client">{{client.clientId}}</td>
</ng-container>
<ng-container matColumnDef="clientName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.oidc_clients.client_name' | i18n}}</th>
<td mat-cell *matCellDef="let client">{{client.clientName}}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let client">
<button mat-icon-button (click)="editClient(client)" [title]="'admin.oidc_clients.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="createNewSecret(client)" [title]="'admin.oidc_clients.new_secret' | i18n">
<mat-icon>vpn_key</mat-icon>
</button>
<button mat-icon-button (click)="deleteClient(client)" [title]="'admin.oidc_clients.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,131 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from 'src/app/services/i18n.service';
import { OidcClientManagementService } from 'src/app/services/admin/oidcclient.management.service';
import { ConfirmDialog } from 'src/app/ui/confirm/confirm.component';
import { AdminOidcClientEditDialog } from './oidc-client.edit';
@Component({
standalone: false,
selector: 'app-admin-oidc-clients',
templateUrl: './oidc-clients.component.html',
styleUrls: ['../admin.scss']
})
export class AdminOidcClientsComponent implements OnInit {
displayedColumns: string[] = ['id', 'clientId', 'clientName', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(
private oidcClientManagementService: OidcClientManagementService,
private i18n: I18nService,
private dialog: MatDialog
) {
this.dataSource = new MatTableDataSource();
}
ngOnInit(): void {
this.loadClients();
}
loadClients(): void {
this.oidcClientManagementService.getClients(this.pageIndex, this.pageSize).subscribe(
(data: any) => {
this.dataSource.data = data.content || data;
this.totalElements = data.totalElements || data.length;
},
error => {
console.error('Error loading OIDC clients:', error);
}
);
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadClients();
}
onSortChange(sort: any): void {
// Sorting can be implemented when backend supports it
console.log('Sort change:', sort);
}
deleteClient(client: any): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.oidc_clients.confirm_delete',
'args': [client.clientName]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.oidcClientManagementService.deleteClient(client.clientName).subscribe(
() => {
this.loadClients();
},
error => {
console.error('Error deleting client:', error);
}
);
}
});
}
createNewSecret(client: any): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.oidc_clients.confirm_new_secret',
'args': [client.clientName]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.oidcClientManagementService.createNewSecret(client.clientName).subscribe(
() => {
this.loadClients();
},
error => {
console.error('Error creating new secret:', error);
}
);
}
});
}
createClient(): void {
const dialogRef = this.dialog.open(AdminOidcClientEditDialog, {
width: '600px',
data: {}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadClients();
}
});
}
editClient(client: any): void {
const dialogRef = this.dialog.open(AdminOidcClientEditDialog, {
width: '600px',
data: { client }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadClients();
}
});
}
}
@@ -0,0 +1,56 @@
<header>
<h3>{{'admin.partey_maps' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary">
<mat-icon>add</mat-icon>
{{'admin.partey_maps.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.partey_maps.id' | i18n}} </th>
<td mat-cell *matCellDef="let map"> {{map.id}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'admin.partey_maps.name' | i18n}} </th>
<td mat-cell *matCellDef="let map"> {{map.name}} </td>
</ng-container>
<ng-container matColumnDef="policyType">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_maps.policy_type' | i18n}} </th>
<td mat-cell *matCellDef="let map"> {{map.policyType}} </td>
</ng-container>
<ng-container matColumnDef="tags">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_maps.tags' | i18n}} </th>
<td mat-cell *matCellDef="let map"> {{map.tags ? map.tags.join(', ') : '-'}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let map">
<button mat-icon-button [title]="'admin.partey_maps.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteParteyMap(map.id)" [title]="'admin.partey_maps.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,88 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from './../../../services/i18n.service';
import { ParteyMapManagementService } from '../../../services/admin/parteymap.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
@Component({
standalone: false,
selector: 'app-admin-partey-maps',
templateUrl: './partey-maps.component.html',
styleUrls: ['../admin.scss']
})
export class AdminParteyMapsComponent implements OnInit {
displayedColumns: string[] = ['id', 'name', 'policyType', 'tags', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
sortField = 'id';
sortDescending = false;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(
private parteyMapManagementService: ParteyMapManagementService,
private i18n: I18nService,
public dialog: MatDialog
) {
this.dataSource = new MatTableDataSource<any>([]);
}
ngOnInit(): void {
this.loadParteyMaps();
}
loadParteyMaps(): void {
this.parteyMapManagementService.getParteyMaps(this.pageIndex, this.pageSize, this.sortField, this.sortDescending)
.subscribe({
next: (data: any) => {
this.dataSource.data = data.content;
this.totalElements = data.totalElements;
},
error: (error) => {
console.error('Error loading Partey maps:', error);
}
});
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadParteyMaps();
}
onSortChange(sort: any): void {
this.sortField = sort.active || 'id';
this.sortDescending = sort.direction === 'desc';
this.loadParteyMaps();
}
deleteParteyMap(id: string): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.partey_maps.confirm_delete',
'args': [id]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.parteyMapManagementService.deleteParteyMap(id).subscribe({
next: () => {
this.loadParteyMaps();
},
error: (error) => {
console.error('Error deleting Partey map:', error);
}
});
}
});
}
}
@@ -0,0 +1,61 @@
<header>
<h3>{{'admin.partey_reports' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="warn" (click)="deleteAllReports()">
<mat-icon>delete_sweep</mat-icon>
{{'admin.partey_reports.delete_all' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.partey_reports.id' | i18n}} </th>
<td mat-cell *matCellDef="let report"> {{report.id}} </td>
</ng-container>
<ng-container matColumnDef="reporterUserUuid">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_reports.reporter' | i18n}} </th>
<td mat-cell *matCellDef="let report"> {{report.reporterUserUuid}} </td>
</ng-container>
<ng-container matColumnDef="reportedUserUuid">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_reports.reported' | i18n}} </th>
<td mat-cell *matCellDef="let report"> {{report.reportedUserUuid}} </td>
</ng-container>
<ng-container matColumnDef="reportWorldSlug">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_reports.world' | i18n}} </th>
<td mat-cell *matCellDef="let report"> {{report.reportWorldSlug}} </td>
</ng-container>
<ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef mat-sort-header="created"> {{'admin.partey_reports.created' | i18n}} </th>
<td mat-cell *matCellDef="let report"> {{report.created | date:'short'}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let report">
<button mat-icon-button [title]="'admin.partey_reports.view' | i18n">
<mat-icon>visibility</mat-icon>
</button>
<button mat-icon-button (click)="deleteParteyReport(report.id)" [title]="'admin.partey_reports.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,110 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from './../../../services/i18n.service';
import { UserReportManagementService } from '../../../services/admin/userreport.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
@Component({
standalone: false,
selector: 'app-admin-partey-reports',
templateUrl: './partey-reports.component.html',
styleUrls: ['../admin.scss']
})
export class AdminParteyReportsComponent implements OnInit {
displayedColumns: string[] = ['id', 'reporterUserUuid', 'reportedUserUuid', 'reportWorldSlug', 'created', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
sortField = 'id';
sortDescending = false;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(
private userReportManagementService: UserReportManagementService,
private i18n: I18nService,
public dialog: MatDialog
) {
this.dataSource = new MatTableDataSource<any>([]);
}
ngOnInit(): void {
this.loadParteyReports();
}
loadParteyReports(): void {
this.userReportManagementService.getParteyUserReports(this.pageIndex, this.pageSize, this.sortField, this.sortDescending)
.subscribe({
next: (data: any) => {
this.dataSource.data = data.content;
this.totalElements = data.totalElements;
},
error: (error) => {
console.error('Error loading Partey reports:', error);
}
});
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadParteyReports();
}
onSortChange(sort: any): void {
this.sortField = sort.active || 'id';
this.sortDescending = sort.direction === 'desc';
this.loadParteyReports();
}
deleteParteyReport(id: number): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.partey_reports.confirm_delete',
'args': [id.toString()]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.userReportManagementService.deleteParteyUserReport(id).subscribe({
next: () => {
this.loadParteyReports();
},
error: (error) => {
console.error('Error deleting Partey report:', error);
}
});
}
});
}
deleteAllReports(): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.partey_reports.confirm_delete_all',
'args': []
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.userReportManagementService.deleteAllParteyUserReports().subscribe({
next: () => {
this.loadParteyReports();
},
error: (error) => {
console.error('Error deleting all Partey reports:', error);
}
});
}
});
}
}
@@ -0,0 +1,61 @@
<header>
<h3>{{'admin.partey_tags' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary">
<mat-icon>add</mat-icon>
{{'admin.partey_tags.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.partey_tags.id' | i18n}} </th>
<td mat-cell *matCellDef="let tag"> {{tag.id}} </td>
</ng-container>
<ng-container matColumnDef="target">
<th mat-header-cell *matHeaderCellDef mat-sort-header="target"> {{'admin.partey_tags.target' | i18n}} </th>
<td mat-cell *matCellDef="let tag"> {{tag.target}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'admin.partey_tags.name' | i18n}} </th>
<td mat-cell *matCellDef="let tag"> {{tag.name}} </td>
</ng-container>
<ng-container matColumnDef="starts">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_tags.starts' | i18n}} </th>
<td mat-cell *matCellDef="let tag"> {{formatDate(tag.starts)}} </td>
</ng-container>
<ng-container matColumnDef="expires">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_tags.expires' | i18n}} </th>
<td mat-cell *matCellDef="let tag"> {{formatDate(tag.expires)}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let tag">
<button mat-icon-button [title]="'admin.partey_tags.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteParteyTag(tag.id)" [title]="'admin.partey_tags.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,92 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from './../../../services/i18n.service';
import { UserTagManagementService } from '../../../services/admin/usertag.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
@Component({
standalone: false,
selector: 'app-admin-partey-tags',
templateUrl: './partey-tags.component.html',
styleUrls: ['../admin.scss']
})
export class AdminParteyTagsComponent implements OnInit {
displayedColumns: string[] = ['id', 'target', 'name', 'starts', 'expires', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
sortField = 'target';
sortDescending = false;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(
private userTagManagementService: UserTagManagementService,
private i18n: I18nService,
public dialog: MatDialog
) {
this.dataSource = new MatTableDataSource<any>([]);
}
ngOnInit(): void {
this.loadParteyTags();
}
loadParteyTags(): void {
this.userTagManagementService.getParteyUserTags(this.pageIndex, this.pageSize, this.sortField, this.sortDescending)
.subscribe({
next: (data: any) => {
this.dataSource.data = data.content;
this.totalElements = data.totalElements;
},
error: (error) => {
console.error('Error loading Partey tags:', error);
}
});
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadParteyTags();
}
onSortChange(sort: any): void {
this.sortField = sort.active || 'target';
this.sortDescending = sort.direction === 'desc';
this.loadParteyTags();
}
deleteParteyTag(id: number): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.partey_tags.confirm_delete',
'args': [id.toString()]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.userTagManagementService.deleteParteyUserTag(id).subscribe({
next: () => {
this.loadParteyTags();
},
error: (error) => {
console.error('Error deleting Partey tag:', error);
}
});
}
});
}
formatDate(date: string): string {
return date ? new Date(date).toLocaleString() : '-';
}
}
@@ -0,0 +1,80 @@
<h2 mat-dialog-title>{{ isEditMode ? ('admin.permission_mappings.edit_mapping' | i18n) : ('admin.permission_mappings.create_mapping' | i18n) }}</h2>
<mat-dialog-content>
<form [formGroup]="mappingForm">
<mat-form-field appearance="outline">
<mat-label>{{'admin.permission_mappings.item' | i18n}}</mat-label>
<input matInput type="number" formControlName="item" required>
@if (mappingForm.get('item')?.hasError('required')) {
<mat-error>
{{'admin.permission_mappings.item_required' | i18n}}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.permission_mappings.product' | i18n}}</mat-label>
<input matInput formControlName="product">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.permission_mappings.names' | i18n}}</mat-label>
<input matInput formControlName="names" required placeholder="permission1, permission2">
<mat-hint>{{'admin.permission_mappings.names_hint' | i18n}}</mat-hint>
@if (mappingForm.get('names')?.hasError('required')) {
<mat-error>
{{'admin.permission_mappings.names_required' | i18n}}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.permission_mappings.lifetime' | i18n}}</mat-label>
<input matInput type="number" formControlName="lifetime">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.permission_mappings.lifetime_unit' | i18n}}</mat-label>
<mat-select formControlName="lifetimeUnit">
@for (unit of lifetimeUnits; track unit) {
<mat-option [value]="unit">{{unit}}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.permission_mappings.starts' | i18n}}</mat-label>
<input matInput type="datetime-local" formControlName="starts">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.permission_mappings.expires' | i18n}}</mat-label>
<input matInput type="datetime-local" formControlName="expires">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.permission_mappings.starts_question' | i18n}}</mat-label>
<input matInput formControlName="startsQuestion">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.permission_mappings.expires_question' | i18n}}</mat-label>
<input matInput formControlName="expiresQuestion">
</mat-form-field>
<mat-checkbox formControlName="addon">
{{'admin.permission_mappings.addon' | i18n}}
</mat-checkbox>
<mat-checkbox formControlName="lifetimeRound">
{{'admin.permission_mappings.lifetime_round' | i18n}}
</mat-checkbox>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="cancel()">{{'admin.cancel' | i18n}}</button>
<button mat-raised-button color="primary" (click)="save()" [disabled]="!mappingForm.valid">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,94 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { PermissionMappingManagementService } from '../../../services/admin/permissionmapping.management.service';
@Component({
standalone: false,
selector: 'admin-permission-mapping-edit',
templateUrl: './permission-mapping.edit.html',
styleUrls: ['../admin.scss']
})
export class AdminPermissionMappingEditDialog implements OnInit {
mappingForm: FormGroup;
isEditMode: boolean;
lifetimeUnits = ['SECONDS', 'MINUTES', 'HOURS', 'DAYS', 'WEEKS', 'MONTHS', 'YEARS'];
constructor(
private dialogRef: MatDialogRef<AdminPermissionMappingEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: { mapping?: any },
private fb: FormBuilder,
private mappingService: PermissionMappingManagementService
) {
this.isEditMode = !!data?.mapping;
}
ngOnInit() {
const mapping = this.data?.mapping;
this.mappingForm = this.fb.group({
product: [mapping?.product || ''],
item: [mapping?.item || null, [Validators.required, Validators.min(0)]],
names: [mapping?.names ? Array.from(mapping.names).join(', ') : '', Validators.required],
addon: [mapping?.addon || false],
lifetime: [mapping?.lifetime || null, Validators.min(0)],
lifetimeUnit: [mapping?.lifetimeUnit || 'DAYS'],
lifetimeRound: [mapping?.lifetimeRound || false],
starts: [mapping?.starts ? this.formatDateTimeLocal(mapping.starts) : ''],
expires: [mapping?.expires ? this.formatDateTimeLocal(mapping.expires) : ''],
startsQuestion: [mapping?.startsQuestion || ''],
expiresQuestion: [mapping?.expiresQuestion || '']
});
}
formatDateTimeLocal(isoString: string): string {
if (!isoString) return '';
const date = new Date(isoString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
parseDateTimeLocal(dateTimeLocal: string): string | null {
if (!dateTimeLocal) return null;
return new Date(dateTimeLocal).toISOString();
}
save() {
if (this.mappingForm.valid) {
const formValue = this.mappingForm.value;
const mappingData = {
product: formValue.product || null,
item: formValue.item,
names: formValue.names.split(',').map((n: string) => n.trim()).filter((n: string) => n),
addon: formValue.addon,
lifetime: formValue.lifetime,
lifetimeUnit: formValue.lifetimeUnit,
lifetimeRound: formValue.lifetimeRound,
starts: this.parseDateTimeLocal(formValue.starts),
expires: this.parseDateTimeLocal(formValue.expires),
startsQuestion: formValue.startsQuestion || null,
expiresQuestion: formValue.expiresQuestion || null
};
if (this.isEditMode) {
mappingData['id'] = this.data.mapping.id;
}
const saveObservable = this.isEditMode
? this.mappingService.update(mappingData)
: this.mappingService.create(mappingData);
saveObservable.subscribe({
next: (result) => this.dialogRef.close(result),
error: (error) => console.error('Error saving permission mapping:', error)
});
}
}
cancel() {
this.dialogRef.close();
}
}
@@ -0,0 +1,60 @@
<header>
<h3>{{'admin.permission_mappings' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary" (click)="createMapping()">
<mat-icon>add</mat-icon>
{{'admin.permission_mappings.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.permission_mappings.id' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.id}}</td>
</ng-container>
<ng-container matColumnDef="item">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.permission_mappings.item' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.item}}</td>
</ng-container>
<ng-container matColumnDef="names">
<th mat-header-cell *matHeaderCellDef>{{'admin.permission_mappings.names' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.names?.join(', ')}}</td>
</ng-container>
<ng-container matColumnDef="lifetime">
<th mat-header-cell *matHeaderCellDef>{{'admin.permission_mappings.lifetime' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.lifetime}} {{mapping.lifetimeUnit}}</td>
</ng-container>
<ng-container matColumnDef="product">
<th mat-header-cell *matHeaderCellDef>{{'admin.permission_mappings.product' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.product}}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">
<button mat-icon-button (click)="editMapping(mapping)" [title]="'admin.permission_mappings.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteMapping(mapping)" [title]="'admin.permission_mappings.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,109 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from 'src/app/services/i18n.service';
import { PermissionMappingManagementService } from 'src/app/services/admin/permissionmapping.management.service';
import { ConfirmDialog } from 'src/app/ui/confirm/confirm.component';
import { AdminPermissionMappingEditDialog } from './permission-mapping.edit';
@Component({
standalone: false,
selector: 'app-admin-permission-mappings',
templateUrl: './permission-mappings.component.html',
styleUrls: ['../admin.scss']
})
export class AdminPermissionMappingsComponent implements OnInit {
displayedColumns: string[] = ['id', 'item', 'names', 'lifetime', 'product', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(
private permissionMappingManagementService: PermissionMappingManagementService,
private i18n: I18nService,
private dialog: MatDialog
) {
this.dataSource = new MatTableDataSource();
}
ngOnInit(): void {
this.loadMappings();
}
loadMappings(): void {
this.permissionMappingManagementService.getPermissionMappings(this.pageIndex, this.pageSize).subscribe(
(data: any) => {
this.dataSource.data = data.content || data;
this.totalElements = data.totalElements || data.length;
},
error => {
console.error('Error loading permission mappings:', error);
}
);
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadMappings();
}
onSortChange(sort: any): void {
// Sorting can be implemented when backend supports it
console.log('Sort change:', sort);
}
createMapping(): void {
const dialogRef = this.dialog.open(AdminPermissionMappingEditDialog, {
width: '700px',
data: {}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadMappings();
}
});
}
editMapping(mapping: any): void {
const dialogRef = this.dialog.open(AdminPermissionMappingEditDialog, {
width: '700px',
data: { mapping: mapping }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadMappings();
}
});
}
deleteMapping(mapping: any): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.permission_mappings.confirm_delete',
'args': [mapping.item]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.permissionMappingManagementService.delete(mapping).subscribe(
() => {
this.loadMappings();
},
error => {
console.error('Error deleting mapping:', error);
}
);
}
});
}
}
@@ -0,0 +1,39 @@
<h2 mat-dialog-title>{{ isEditMode ? ('admin.permission.edit' | i18n) : ('admin.permission.create' | i18n) }}</h2>
<mat-dialog-content>
<form [formGroup]="form">
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.permissions.name' | i18n }}</mat-label>
<input matInput formControlName="name" required>
<mat-error>
{{ 'admin.permissions.error.name' | i18n }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.permissions.starts' | i18n }}</mat-label>
<input matInput type="datetime-local" formControlName="starts">
<mat-error>
{{ 'admin.permissions.error.starts' | i18n }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.permissions.expires' | i18n }}</mat-label>
<input matInput type="datetime-local" formControlName="expires">
<mat-error>
{{ 'admin.permissions.error.expires' | i18n }}
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="addon">
{{ 'admin.permissions.addon' | i18n }}
</mat-checkbox>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<a mat-button [mat-dialog-close]="false">{{ 'cancel' | i18n }}</a>
<a [disabled]="form.invalid" mat-raised-button (click)="save()" color="accent">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</a>
</mat-dialog-actions>
@@ -0,0 +1,97 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { PermissionManagementService } from '../../../services/admin/permission.management.service';
import { UserManagementService } from '../../../services/admin/user.management.service';
import { I18nService } from './../../../services/i18n.service';
@Component({
standalone: false,
selector: 'app-admin-permission-edit-dialog',
templateUrl: './permission.edit.html',
styleUrls: ['../admin.scss']
})
export class AdminPermissionEditDialog implements OnInit {
form: FormGroup;
permission: any;
username: string;
isEditMode: boolean = true;
constructor(
private permissionManagementService: PermissionManagementService,
private userManagementService: UserManagementService,
private i18n: I18nService,
private formBuilder: FormBuilder,
public dialogRef: MatDialogRef<AdminPermissionEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.username = data.username;
this.permission = data.permission ? JSON.parse(JSON.stringify(data.permission)) : {};
this.isEditMode = !!this.permission.id;
}
ngOnInit(): void {
this.form = this.formBuilder.group({
name: [this.permission.name || '', Validators.required],
addon: [this.permission.addon || false],
starts: [this.permission.starts ? this.formatDateTimeLocal(this.permission.starts) : ''],
expires: [this.permission.expires ? this.formatDateTimeLocal(this.permission.expires) : '']
});
}
formatDateTimeLocal(isoString: string): string {
if (!isoString) return '';
const date = new Date(isoString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
parseDateTimeLocal(dateTimeLocal: string): string | null {
if (!dateTimeLocal) return null;
return new Date(dateTimeLocal).toISOString();
}
save(): void {
if (this.form.invalid) {
return;
}
// Get user by username to retrieve target ID
this.userManagementService.getUserByUsername(this.username).subscribe({
next: (user: any) => {
const permissionData = {
name: this.form.value.name,
target: user.id,
addon: this.form.value.addon,
starts: this.parseDateTimeLocal(this.form.value.starts),
expires: this.parseDateTimeLocal(this.form.value.expires)
};
if (this.isEditMode) {
permissionData['id'] = this.permission.id;
}
const saveObservable = this.isEditMode
? this.permissionManagementService.updatePermission(permissionData)
: this.permissionManagementService.createPermission(permissionData);
saveObservable.subscribe({
next: (result) => {
this.dialogRef.close(result);
},
error: (error) => {
console.error('Error saving permission:', error);
}
});
},
error: (error) => {
console.error('Error getting user:', error);
}
});
}
}
@@ -0,0 +1,74 @@
<header>
<h3>{{'admin.permissions' | i18n}}@if (selectedUsername) { - {{selectedUsername}}}</h3>
<span class="spacer"></span>
@if (selectedUsername) {
<button mat-raised-button color="primary" (click)="createPermission()">
<mat-icon>add</mat-icon>
{{'admin.permissions.create' | i18n}}
</button>
}
</header>
@if (loading) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
}
@if (selectedUsername) {
<div>
@if (permissions.length > 0) {
<div class="mat-elevation-z8">
<table mat-table [dataSource]="permissions">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> {{'admin.permissions.id' | i18n}} </th>
<td mat-cell *matCellDef="let permission"> {{permission.id}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{'admin.permissions.name' | i18n}} </th>
<td mat-cell *matCellDef="let permission"> {{permission.name}} </td>
</ng-container>
<ng-container matColumnDef="addon">
<th mat-header-cell *matHeaderCellDef> {{'admin.permissions.addon' | i18n}} </th>
<td mat-cell *matCellDef="let permission">
@if (permission.addon) {
<mat-icon color="primary">check</mat-icon>
}
</td>
</ng-container>
<ng-container matColumnDef="starts">
<th mat-header-cell *matHeaderCellDef> {{'admin.permissions.starts' | i18n}} </th>
<td mat-cell *matCellDef="let permission"> {{formatDate(permission.starts)}} </td>
</ng-container>
<ng-container matColumnDef="expires">
<th mat-header-cell *matHeaderCellDef> {{'admin.permissions.expires' | i18n}} </th>
<td mat-cell *matCellDef="let permission"> {{formatDate(permission.expires)}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let permission">
<button mat-icon-button (click)="editPermission(permission)" [title]="'admin.permissions.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deletePermission(permission)" [title]="'admin.permissions.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
@if (!loading && permissions.length === 0) {
<p style="text-align: center; margin-top: 20px;">{{'admin.permissions.no_permissions' | i18n}}</p>
}
</div>
}
@@ -0,0 +1,113 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { I18nService } from './../../../services/i18n.service';
import { PermissionManagementService } from '../../../services/admin/permission.management.service';
import { UserManagementService } from '../../../services/admin/user.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
import { AdminPermissionEditDialog } from './permission.edit';
@Component({
standalone: false,
selector: 'app-admin-permissions',
templateUrl: './permissions.component.html',
styleUrls: ['../admin.scss']
})
export class AdminPermissionsComponent implements OnInit {
permissions: any[] = [];
loading: boolean = false;
selectedUsername: string = '';
displayedColumns: string[] = ['id', 'name', 'addon', 'starts', 'expires', 'actions'];
constructor(
private permissionManagementService: PermissionManagementService,
private userManagementService: UserManagementService,
private i18n: I18nService,
public dialog: MatDialog,
private route: ActivatedRoute
) {}
ngOnInit(): void {
// Check for username query parameter
this.route.queryParams.subscribe(params => {
if (params['username']) {
this.selectedUsername = params['username'];
this.loadPermissions();
} else {
this.permissions = [];
this.loading = false;
}
});
}
loadPermissions(): void {
this.loading = true;
this.permissionManagementService.getPermissionsForUser(this.selectedUsername, 'name', true)
.subscribe({
next: (data: any) => {
this.permissions = data;
this.loading = false;
},
error: (error) => {
console.error('Error loading permissions:', error);
this.loading = false;
this.permissions = [];
}
});
}
createPermission(): void {
const dialogRef = this.dialog.open(AdminPermissionEditDialog, {
data: { username: this.selectedUsername, permission: null },
minWidth: '400px'
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadPermissions();
}
});
}
editPermission(permission: any): void {
const dialogRef = this.dialog.open(AdminPermissionEditDialog, {
data: { username: this.selectedUsername, permission: permission },
minWidth: '400px'
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadPermissions();
}
});
}
deletePermission(permission: any): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.permissions.confirm_delete',
'args': [permission.id.toString()]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.permissionManagementService.deletePermission(permission).subscribe({
next: () => {
this.loadPermissions();
},
error: (error) => {
console.error('Error deleting permission:', error);
}
});
}
});
}
formatDate(date: string): string {
return date ? new Date(date).toLocaleString() : '-';
}
}
@@ -0,0 +1,68 @@
<h2 mat-dialog-title>{{ isEditMode ? ('admin.quota_mappings.edit_mapping' | i18n) :
('admin.quota_mappings.create_mapping' | i18n) }}</h2>
<mat-dialog-content>
<form [formGroup]="mappingForm">
<mat-form-field appearance="outline">
<mat-label>{{'admin.quota_mappings.name' | i18n}}</mat-label>
<input matInput formControlName="name" required>
@if (mappingForm.get('name')?.hasError('required')) {
<mat-error>
{{'admin.quota_mappings.name_required' | i18n}}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.quota_mappings.value' | i18n}}</mat-label>
<input matInput type="number" formControlName="value" required>
@if (mappingForm.get('value')?.hasError('required')) {
<mat-error>
{{'admin.quota_mappings.value_required' | i18n}}
</mat-error>
}
@if (mappingForm.get('value')?.hasError('min')) {
<mat-error>
{{'admin.quota_mappings.value_min' | i18n}}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.quota_mappings.unit' | i18n}}</mat-label>
<input matInput formControlName="unit">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.quota_mappings.items' | i18n}}</mat-label>
<input matInput formControlName="items" required placeholder="1, 2, 3">
<mat-hint>{{'admin.quota_mappings.items_hint' | i18n}}</mat-hint>
@if (mappingForm.get('items')?.hasError('required')) {
<mat-error>
{{'admin.quota_mappings.items_required' | i18n}}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.quota_mappings.products' | i18n}}</mat-label>
<input matInput formControlName="products" placeholder="product1, product2">
<mat-hint>{{'admin.quota_mappings.products_hint' | i18n}}</mat-hint>
</mat-form-field>
<mat-checkbox formControlName="append">
{{'admin.quota_mappings.append' | i18n}}
</mat-checkbox>
<mat-checkbox formControlName="disposable">
{{'admin.quota_mappings.disposable' | i18n}}
</mat-checkbox>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="cancel()">{{'admin.cancel' | i18n}}</button>
<button mat-raised-button color="primary" (click)="save()" [disabled]="!mappingForm.valid">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,73 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { QuotaMappingManagementService } from '../../../services/admin/quotamapping.management.service';
@Component({
standalone: false,
selector: 'admin-quota-mapping-edit',
templateUrl: './quota-mapping.edit.html',
styleUrls: ['../admin.scss']
})
export class AdminQuotaMappingEditDialog implements OnInit {
mappingForm: FormGroup;
isEditMode: boolean;
constructor(
private dialogRef: MatDialogRef<AdminQuotaMappingEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: { mapping?: any },
private fb: FormBuilder,
private mappingService: QuotaMappingManagementService
) {
this.isEditMode = !!data?.mapping;
}
ngOnInit() {
const mapping = this.data?.mapping;
this.mappingForm = this.fb.group({
products: [mapping?.products ? Array.from(mapping.products).join(', ') : ''],
items: [mapping?.items ? Array.from(mapping.items).join(', ') : '', Validators.required],
name: [mapping?.name || '', Validators.required],
value: [mapping?.value || 0, [Validators.required, Validators.min(0)]],
unit: [mapping?.unit || ''],
append: [mapping?.append || false],
disposable: [mapping?.disposable || false]
});
}
save() {
if (this.mappingForm.valid) {
const formValue = this.mappingForm.value;
const mappingData = {
products: formValue.products
? formValue.products.split(',').map((p: string) => p.trim()).filter((p: string) => p)
: [],
items: formValue.items
? formValue.items.split(',').map((i: string) => parseInt(i.trim())).filter((i: number) => !isNaN(i))
: [],
name: formValue.name,
value: formValue.value,
unit: formValue.unit || null,
append: formValue.append,
disposable: formValue.disposable
};
if (this.isEditMode) {
mappingData['id'] = this.data.mapping.id;
}
const saveObservable = this.isEditMode
? this.mappingService.update(mappingData)
: this.mappingService.create(mappingData);
saveObservable.subscribe({
next: (result) => this.dialogRef.close(result),
error: (error) => console.error('Error saving quota mapping:', error)
});
}
}
cancel() {
this.dialogRef.close();
}
}
@@ -0,0 +1,60 @@
<header>
<h3>{{'admin.quota_mappings' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary" (click)="createMapping()">
<mat-icon>add</mat-icon>
{{'admin.quota_mappings.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.quota_mappings.id' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.id}}</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.quota_mappings.name' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.name}}</td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.quota_mappings.value' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.value}}</td>
</ng-container>
<ng-container matColumnDef="unit">
<th mat-header-cell *matHeaderCellDef>{{'admin.quota_mappings.unit' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.unit}}</td>
</ng-container>
<ng-container matColumnDef="items">
<th mat-header-cell *matHeaderCellDef>{{'admin.quota_mappings.items' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.items?.join(', ')}}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">
<button mat-icon-button (click)="editMapping(mapping)" [title]="'admin.quota_mappings.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteMapping(mapping)" [title]="'admin.quota_mappings.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,109 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from 'src/app/services/i18n.service';
import { QuotaMappingManagementService } from 'src/app/services/admin/quotamapping.management.service';
import { ConfirmDialog } from 'src/app/ui/confirm/confirm.component';
import { AdminQuotaMappingEditDialog } from './quota-mapping.edit';
@Component({
standalone: false,
selector: 'app-admin-quota-mappings',
templateUrl: './quota-mappings.component.html',
styleUrls: ['../admin.scss']
})
export class AdminQuotaMappingsComponent implements OnInit {
displayedColumns: string[] = ['id', 'name', 'value', 'unit', 'items', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(
private quotaMappingManagementService: QuotaMappingManagementService,
private i18n: I18nService,
private dialog: MatDialog
) {
this.dataSource = new MatTableDataSource();
}
ngOnInit(): void {
this.loadMappings();
}
loadMappings(): void {
this.quotaMappingManagementService.getQuotaMappings(this.pageIndex, this.pageSize).subscribe(
(data: any) => {
this.dataSource.data = data.content || data;
this.totalElements = data.totalElements || data.length;
},
error => {
console.error('Error loading quota mappings:', error);
}
);
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadMappings();
}
onSortChange(sort: any): void {
// Sorting can be implemented when backend supports it
console.log('Sort change:', sort);
}
createMapping(): void {
const dialogRef = this.dialog.open(AdminQuotaMappingEditDialog, {
width: '700px',
data: {}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadMappings();
}
});
}
editMapping(mapping: any): void {
const dialogRef = this.dialog.open(AdminQuotaMappingEditDialog, {
width: '700px',
data: { mapping: mapping }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadMappings();
}
});
}
deleteMapping(mapping: any): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.quota_mappings.confirm_delete',
'args': [mapping.name]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.quotaMappingManagementService.delete(mapping).subscribe(
() => {
this.loadMappings();
},
error => {
console.error('Error deleting mapping:', error);
}
);
}
});
}
}
@@ -0,0 +1,46 @@
<h2 mat-dialog-title>{{ isEditMode ? ('admin.quotas.edit_quota' | i18n) : ('admin.quotas.create_quota' | i18n) }}</h2>
<mat-dialog-content>
<form [formGroup]="quotaForm">
<mat-form-field appearance="outline">
<mat-label>{{'admin.quotas.name' | i18n}}</mat-label>
<input matInput formControlName="name" required>
@if (quotaForm.get('name').hasError('required')) {
<mat-error>
{{'admin.quotas.name_required' | i18n}}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.quotas.value' | i18n}}</mat-label>
<input matInput type="number" formControlName="value" required>
@if (quotaForm.get('value').hasError('required')) {
<mat-error>
{{'admin.quotas.value_required' | i18n}}
</mat-error>
}
@if (quotaForm.get('value').hasError('min')) {
<mat-error>
{{'admin.quotas.value_min' | i18n}}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.quotas.unit' | i18n}}</mat-label>
<input matInput formControlName="unit">
</mat-form-field>
<mat-checkbox formControlName="disposable">
{{'admin.quotas.disposable' | i18n}}
</mat-checkbox>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="cancel()">{{'admin.cancel' | i18n}}</button>
<button mat-raised-button color="primary" (click)="save()" [disabled]="!quotaForm.valid">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</button>
</mat-dialog-actions>
+72
View File
@@ -0,0 +1,72 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { QuotaManagementService } from 'src/app/services/admin/quota.management.service';
import { UserManagementService } from 'src/app/services/admin/user.management.service';
@Component({
standalone: false,
selector: 'admin-quota-edit',
templateUrl: './quota.edit.html',
styleUrls: ['../admin.scss']
})
export class AdminQuotaEditDialog implements OnInit {
quotaForm: FormGroup;
isEditMode: boolean;
constructor(
private dialogRef: MatDialogRef<AdminQuotaEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: { username: string; quota?: any },
private fb: FormBuilder,
private quotaManagementService: QuotaManagementService,
private userManagementService: UserManagementService
) {
this.isEditMode = !!data.quota;
}
ngOnInit() {
const quota = this.data.quota;
this.quotaForm = this.fb.group({
name: [quota?.name || '', Validators.required],
value: [quota?.value || null, [Validators.required, Validators.min(0)]],
unit: [quota?.unit || ''],
disposable: [quota?.disposable || false]
});
}
save() {
if (this.quotaForm.valid) {
// Get user by username to retrieve target ID
this.userManagementService.getUserByUsername(this.data.username).subscribe({
next: (user: any) => {
const formValue = this.quotaForm.value;
const quotaData = {
target: user.id,
name: formValue.name,
value: formValue.value,
unit: formValue.unit,
disposable: formValue.disposable
};
if (this.isEditMode) {
quotaData['id'] = this.data.quota.id;
}
const saveObservable = this.isEditMode
? this.quotaManagementService.updateQuota(quotaData)
: this.quotaManagementService.createQuota(quotaData);
saveObservable.subscribe({
next: (result) => this.dialogRef.close(result),
error: (error) => console.error('Error saving quota:', error)
});
},
error: (error) => console.error('Error getting user:', error)
});
}
}
cancel() {
this.dialogRef.close();
}
}
@@ -0,0 +1,74 @@
<header>
<h3>{{'admin.quotas' | i18n}}@if (selectedUsername) { - {{selectedUsername}}}</h3>
<span class="spacer"></span>
@if (selectedUsername) {
<button mat-raised-button color="primary" (click)="createQuota()">
<mat-icon>add</mat-icon>
{{'admin.quotas.create' | i18n}}
</button>
}
</header>
@if (loading) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
}
@if (selectedUsername) {
<div>
@if (quotas.length > 0) {
<div class="mat-elevation-z8">
<table mat-table [dataSource]="quotas">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> {{'admin.quotas.id' | i18n}} </th>
<td mat-cell *matCellDef="let quota"> {{quota.id}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{'admin.quotas.name' | i18n}} </th>
<td mat-cell *matCellDef="let quota"> {{quota.name}} </td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef> {{'admin.quotas.value' | i18n}} </th>
<td mat-cell *matCellDef="let quota"> {{quota.value}} </td>
</ng-container>
<ng-container matColumnDef="unit">
<th mat-header-cell *matHeaderCellDef> {{'admin.quotas.unit' | i18n}} </th>
<td mat-cell *matCellDef="let quota"> {{quota.unit || '-'}} </td>
</ng-container>
<ng-container matColumnDef="disposable">
<th mat-header-cell *matHeaderCellDef> {{'admin.quotas.disposable' | i18n}} </th>
<td mat-cell *matCellDef="let quota">
@if (quota.disposable) {
<mat-icon>check</mat-icon>
}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let quota">
<button mat-icon-button (click)="editQuota(quota)" [title]="'admin.quotas.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteQuota(quota)" [title]="'admin.quotas.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
@if (!loading && quotas.length === 0) {
<p style="text-align: center; margin-top: 20px;">{{'admin.quotas.no_quotas' | i18n}}</p>
}
</div>
}
@@ -0,0 +1,115 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { I18nService } from './../../../services/i18n.service';
import { QuotaManagementService } from '../../../services/admin/quota.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
import { AdminQuotaEditDialog } from './quota.edit';
@Component({
standalone: false,
selector: 'app-admin-quotas',
templateUrl: './quotas.component.html',
styleUrls: ['../admin.scss']
})
export class AdminQuotasComponent implements OnInit {
quotas: any[] = [];
loading: boolean = false;
selectedUsername: string = '';
displayedColumns: string[] = ['id', 'name', 'value', 'unit', 'disposable', 'actions'];
constructor(
private quotaManagementService: QuotaManagementService,
private i18n: I18nService,
public dialog: MatDialog,
private route: ActivatedRoute
) {}
ngOnInit(): void {
// Check for username query parameter
this.route.queryParams.subscribe(params => {
if (params['username']) {
this.selectedUsername = params['username'];
this.loadQuotas();
} else {
this.quotas = [];
this.loading = false;
}
});
}
loadQuotas(): void {
this.loading = true;
this.quotaManagementService.getQuotasForUser(this.selectedUsername, 'name')
.subscribe({
next: (data: any) => {
this.quotas = data;
this.loading = false;
},
error: (error) => {
console.error('Error loading quotas:', error);
this.loading = false;
this.quotas = [];
}
});
}
deleteQuota(quota: any): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.quotas.confirm_delete',
'args': [quota.id.toString()]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.quotaManagementService.deleteQuota(quota).subscribe({
next: () => {
this.loadQuotas();
},
error: (error) => {
console.error('Error deleting quota:', error);
}
});
}
});
}
formatDate(date: string): string {
return date ? new Date(date).toLocaleString() : '-';
}
createQuota(): void {
if (!this.selectedUsername) {
return;
}
const dialogRef = this.dialog.open(AdminQuotaEditDialog, {
width: '500px',
data: { username: this.selectedUsername }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadQuotas();
}
});
}
editQuota(quota: any): void {
const dialogRef = this.dialog.open(AdminQuotaEditDialog, {
width: '500px',
data: { username: this.selectedUsername, quota: quota }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadQuotas();
}
});
}
}
@@ -0,0 +1,50 @@
<h2 mat-dialog-title>{{ isEditMode ? ('admin.services.edit_service' | i18n) : ('admin.services.create_service' | i18n) }}</h2>
<mat-dialog-content>
<form [formGroup]="serviceForm">
<mat-form-field appearance="outline">
<mat-label>{{'admin.services.name' | i18n}}</mat-label>
<input matInput formControlName="name" required>
@if (serviceForm.get('name')?.hasError('required')) {
<mat-error>
{{'admin.services.name_required' | i18n}}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.services.url' | i18n}}</mat-label>
<input matInput formControlName="url" required>
@if (serviceForm.get('url')?.hasError('required')) {
<mat-error>
{{'admin.services.url_required' | i18n}}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.services.permission' | i18n}}</mat-label>
<input matInput formControlName="permission">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.services.category' | i18n}}</mat-label>
<input matInput formControlName="category">
</mat-form-field>
<mat-checkbox formControlName="alwaysPermitted">
{{'admin.services.always_permitted' | i18n}}
</mat-checkbox>
<mat-checkbox formControlName="sameSite">
{{'admin.services.same_site' | i18n}}
</mat-checkbox>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="cancel()">{{'admin.cancel' | i18n}}</button>
<button mat-raised-button color="primary" (click)="save()" [disabled]="!serviceForm.valid">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,59 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ServiceManagementService } from '../../../services/admin/service.management.service';
@Component({
standalone: false,
selector: 'admin-service-edit',
templateUrl: './service.edit.html',
styleUrls: ['../admin.scss']
})
export class AdminServiceEditDialog implements OnInit {
serviceForm: FormGroup;
isEditMode: boolean;
constructor(
private dialogRef: MatDialogRef<AdminServiceEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: { service?: any },
private fb: FormBuilder,
private serviceManagementService: ServiceManagementService
) {
this.isEditMode = !!data?.service;
}
ngOnInit() {
const service = this.data?.service;
this.serviceForm = this.fb.group({
name: [{ value: service?.name || '', disabled: this.isEditMode }, Validators.required],
url: [service?.url || '', Validators.required],
alwaysPermitted: [service?.alwaysPermitted || false],
sameSite: [service?.sameSite || false],
permission: [service?.permission || ''],
category: [service?.category || '']
});
}
save() {
if (this.serviceForm.valid) {
const formValue = this.serviceForm.getRawValue();
const serviceData = {
name: formValue.name,
url: formValue.url,
alwaysPermitted: formValue.alwaysPermitted,
sameSite: formValue.sameSite,
permission: formValue.permission,
category: formValue.category
};
this.serviceManagementService.createOrUpdateService(serviceData).subscribe({
next: (result) => this.dialogRef.close(result),
error: (error) => console.error('Error saving service:', error)
});
}
}
cancel() {
this.dialogRef.close();
}
}
@@ -0,0 +1,56 @@
<header>
<h3>{{'admin.services' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary" (click)="createService()">
<mat-icon>add</mat-icon>
{{'admin.services.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{'admin.services.name' | i18n}} </th>
<td mat-cell *matCellDef="let service"> {{service.name}} </td>
</ng-container>
<ng-container matColumnDef="url">
<th mat-header-cell *matHeaderCellDef> {{'admin.services.url' | i18n}} </th>
<td mat-cell *matCellDef="let service"> {{service.url}} </td>
</ng-container>
<ng-container matColumnDef="category">
<th mat-header-cell *matHeaderCellDef> {{'admin.services.category' | i18n}} </th>
<td mat-cell *matCellDef="let service"> {{service.category}} </td>
</ng-container>
<ng-container matColumnDef="permission">
<th mat-header-cell *matHeaderCellDef> {{'admin.services.permission' | i18n}} </th>
<td mat-cell *matCellDef="let service"> {{service.permission}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let service">
<button mat-icon-button (click)="editService(service)" [title]="'admin.services.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteService(service)" [title]="'admin.services.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,105 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from './../../../services/i18n.service';
import { ServiceManagementService } from '../../../services/admin/service.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
import { AdminServiceEditDialog } from './service.edit';
@Component({
standalone: false,
selector: 'app-admin-services',
templateUrl: './services.component.html',
styleUrls: ['../admin.scss']
})
export class AdminServicesComponent implements OnInit {
displayedColumns: string[] = ['name', 'url', 'category', 'permission', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
@ViewChild(MatPaginator) paginator: MatPaginator;
constructor(
private serviceManagementService: ServiceManagementService,
private i18n: I18nService,
public dialog: MatDialog
) {
this.dataSource = new MatTableDataSource<any>([]);
}
ngOnInit(): void {
this.loadServices();
}
loadServices(): void {
this.serviceManagementService.getAllServices(this.pageIndex, this.pageSize)
.subscribe({
next: (data: any) => {
this.dataSource.data = data.content;
this.totalElements = data.totalElements;
},
error: (error) => {
console.error('Error loading services:', error);
}
});
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadServices();
}
createService(): void {
const dialogRef = this.dialog.open(AdminServiceEditDialog, {
width: '600px',
data: {}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadServices();
}
});
}
editService(service: any): void {
const dialogRef = this.dialog.open(AdminServiceEditDialog, {
width: '600px',
data: { service: service }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadServices();
}
});
}
deleteService(service: any): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.services.confirm_delete',
'args': [service.name]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.serviceManagementService.deleteService(service).subscribe({
next: () => {
this.loadServices();
},
error: (error) => {
console.error('Error deleting service:', error);
}
});
}
});
}
}
@@ -0,0 +1,64 @@
<h2 mat-dialog-title>{{ (isEditMode ? 'admin.shortened_urls.edit' : 'admin.shortened_urls.create') | i18n }}</h2>
<mat-dialog-content>
<form [formGroup]="form">
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.shortened_urls.code' | i18n }}</mat-label>
<input matInput formControlName="code" [placeholder]="'admin.shortened_urls.code' | i18n" required>
@if (form.get('code')?.hasError('required')) {
<mat-error>
{{ 'admin.shortened_urls.code' | i18n }} {{ 'admin.users.error.username' | i18n }}
</mat-error>
}
@if (isEditMode) {
<mat-hint>Code cannot be changed after creation</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.shortened_urls.owner' | i18n }}</mat-label>
<input matInput formControlName="owner" type="number" [placeholder]="'admin.shortened_urls.owner' | i18n" required>
@if (form.get('owner')?.hasError('required')) {
<mat-error>
{{ 'admin.shortened_urls.owner' | i18n }} is required
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.shortened_urls.url' | i18n }}</mat-label>
<input matInput formControlName="url" [placeholder]="'admin.shortened_urls.url' | i18n" required>
@if (form.get('url')?.hasError('required')) {
<mat-error>
{{ 'admin.shortened_urls.url' | i18n }} is required
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'urlshortener.expires' | i18n }}</mat-label>
<input matInput formControlName="expires" type="datetime-local" [placeholder]="'urlshortener.expires' | i18n">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'urlshortener.password' | i18n }}</mat-label>
<input matInput formControlName="password" type="password" [placeholder]="'urlshortener.password' | i18n">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'urlshortener.note' | i18n }}</mat-label>
<textarea matInput formControlName="note" rows="3" [placeholder]="'urlshortener.note' | i18n"></textarea>
</mat-form-field>
<mat-checkbox formControlName="queryParameters">
{{ 'urlshortener.queryParameters' | i18n }}
</mat-checkbox>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">{{ 'admin.cancel' | i18n }}</button>
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!form.valid">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,77 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ShortenedUrlManagementService } from '../../../services/admin/shortenedurl.management.service';
@Component({
standalone: false,
selector: 'app-admin-shortened-url-edit',
templateUrl: './shortened-url.edit.html',
styleUrls: ['../admin.scss']
})
export class AdminShortenedUrlEditDialog implements OnInit {
form: FormGroup;
isEditMode: boolean = false;
constructor(
public dialogRef: MatDialogRef<AdminShortenedUrlEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: any,
private formBuilder: FormBuilder,
private shortenedUrlManagementService: ShortenedUrlManagementService
) {
this.isEditMode = !!data?.shortenedUrl;
this.form = this.formBuilder.group({
code: [{ value: '', disabled: this.isEditMode }, [Validators.required]],
owner: ['', [Validators.required]],
url: ['', [Validators.required]],
expires: [''],
password: [''],
note: [''],
queryParameters: [false]
});
}
ngOnInit(): void {
if (this.isEditMode && this.data.shortenedUrl) {
const url = this.data.shortenedUrl;
this.form.patchValue({
code: url.code,
owner: url.owner,
url: url.url,
expires: url.expires,
note: url.note,
queryParameters: url.queryParameters || false
});
}
}
onCancel(): void {
this.dialogRef.close();
}
onSubmit(): void {
if (this.form.valid) {
const formValue = this.form.getRawValue();
const shortenedUrl = {
code: formValue.code,
owner: formValue.owner,
url: formValue.url,
expires: formValue.expires,
password: formValue.password,
note: formValue.note,
queryParameters: formValue.queryParameters
};
this.shortenedUrlManagementService.createOrUpdateShortenedUrl(shortenedUrl).subscribe({
next: () => {
this.dialogRef.close(true);
},
error: (error) => {
console.error('Error saving shortened URL:', error);
}
});
}
}
}
@@ -0,0 +1,73 @@
<header>
<h3>{{'admin.shortened_urls' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary" (click)="createShortenedUrl()">
<mat-icon>add</mat-icon>
{{'admin.shortened_urls.create' | i18n}}
</button>
</header>
<mat-card>
<mat-card-content>
<form [formGroup]="searchForm" (ngSubmit)="onSearch()">
<mat-form-field appearance="outline" style="width: 100%; max-width: 400px;">
<mat-label>{{'admin.shortened_urls.search' | i18n}}</mat-label>
<input matInput formControlName="search" [placeholder]="'admin.shortened_urls.search_placeholder' | i18n">
</mat-form-field>
<button mat-raised-button color="primary" type="submit" style="margin-left: 10px;">
<mat-icon>search</mat-icon>
{{'admin.shortened_urls.search' | i18n}}
</button>
</form>
</mat-card-content>
</mat-card>
<br>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="code">
<th mat-header-cell *matHeaderCellDef> {{'admin.shortened_urls.code' | i18n}} </th>
<td mat-cell *matCellDef="let url"> {{url.code}} </td>
</ng-container>
<ng-container matColumnDef="url">
<th mat-header-cell *matHeaderCellDef> {{'admin.shortened_urls.url' | i18n}} </th>
<td mat-cell *matCellDef="let url"> {{url.url}} </td>
</ng-container>
<ng-container matColumnDef="owner">
<th mat-header-cell *matHeaderCellDef> {{'admin.shortened_urls.owner' | i18n}} </th>
<td mat-cell *matCellDef="let url"> {{url.owner}} </td>
</ng-container>
<ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef> {{'admin.shortened_urls.created' | i18n}} </th>
<td mat-cell *matCellDef="let url"> {{url.created | date:'short'}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let url">
<button mat-icon-button (click)="editShortenedUrl(url)" [title]="'admin.shortened_urls.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteShortenedUrl(url.code)" [title]="'admin.shortened_urls.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,118 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { FormBuilder, FormGroup } from '@angular/forms';
import { I18nService } from './../../../services/i18n.service';
import { ShortenedUrlManagementService } from '../../../services/admin/shortenedurl.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
import { AdminShortenedUrlEditDialog } from './shortened-url.edit';
@Component({
standalone: false,
selector: 'app-admin-shortened-urls',
templateUrl: './shortened-urls.component.html',
styleUrls: ['../admin.scss']
})
export class AdminShortenedUrlsComponent implements OnInit {
displayedColumns: string[] = ['code', 'url', 'owner', 'created', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
searchText: string = '';
searchForm: FormGroup;
@ViewChild(MatPaginator) paginator: MatPaginator;
constructor(
private shortenedUrlManagementService: ShortenedUrlManagementService,
private i18n: I18nService,
private formBuilder: FormBuilder,
public dialog: MatDialog
) {
this.dataSource = new MatTableDataSource<any>([]);
this.searchForm = this.formBuilder.group({
search: ['']
});
}
ngOnInit(): void {
this.loadShortenedUrls();
}
loadShortenedUrls(): void {
this.shortenedUrlManagementService.getShortenedUrls(this.pageIndex, this.pageSize, this.searchText)
.subscribe({
next: (data: any) => {
this.dataSource.data = data.content;
this.totalElements = data.totalElements;
},
error: (error) => {
console.error('Error loading shortened URLs:', error);
}
});
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadShortenedUrls();
}
onSearch(): void {
this.searchText = this.searchForm.value.search;
this.pageIndex = 0;
this.loadShortenedUrls();
}
createShortenedUrl(): void {
const dialogRef = this.dialog.open(AdminShortenedUrlEditDialog, {
width: '600px'
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadShortenedUrls();
}
});
}
editShortenedUrl(url: any): void {
const dialogRef = this.dialog.open(AdminShortenedUrlEditDialog, {
width: '600px',
data: { shortenedUrl: url }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadShortenedUrls();
}
});
}
deleteShortenedUrl(code: string, quota: boolean = false): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.shortened_urls.confirm_delete',
'args': [code]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.shortenedUrlManagementService.deleteShortenedUrl(code, quota).subscribe({
next: () => {
this.loadShortenedUrls();
},
error: (error) => {
console.error('Error deleting shortened URL:', error);
}
});
}
});
}
}
@@ -0,0 +1,45 @@
<h2 mat-dialog-title>{{ (isEditMode ? 'admin.system_profile_fields.edit' : 'admin.system_profile_fields.create') | i18n }}</h2>
<mat-dialog-content>
<form [formGroup]="form">
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.system_profile_fields.name' | i18n }}</mat-label>
<input matInput formControlName="name" [placeholder]="'admin.system_profile_fields.name' | i18n" required>
@if (form.get('name')?.hasError('required')) {
<mat-error>
{{ 'admin.system_profile_fields.name_required' | i18n }}
</mat-error>
}
@if (isEditMode) {
<mat-hint>{{ 'admin.system_profile_fields.name_readonly' | i18n }}</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.system_profile_fields.type' | i18n }}</mat-label>
<mat-select formControlName="type" required>
@for (type of profileFieldTypes; track type) {
<mat-option [value]="type">
{{ type }}
</mat-option>
}
</mat-select>
@if (form.get('type')?.hasError('required')) {
<mat-error>
{{ 'admin.system_profile_fields.type_required' | i18n }}
</mat-error>
}
</mat-form-field>
<mat-checkbox formControlName="uniqueValue">
{{ 'admin.system_profile_fields.unique_value' | i18n }}
</mat-checkbox>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">{{ 'admin.cancel' | i18n }}</button>
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!form.valid">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,56 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { SystemProfileFieldManagementService } from '../../../services/admin/systemprofilefield.management.service';
@Component({
standalone: false,
selector: 'admin-system-profile-field-edit-dialog',
templateUrl: './system-profile-field.edit.html',
styleUrls: ['../admin.scss']
})
export class AdminSystemProfileFieldEditDialog implements OnInit {
form: FormGroup;
isEditMode: boolean = false;
profileFieldTypes = ['TEXT', 'NUMBER', 'DATE', 'URL', 'EMAIL', 'BOOL', 'BLOB', 'DATETIME', 'TIME'];
constructor(
private fb: FormBuilder,
private systemProfileFieldManagementService: SystemProfileFieldManagementService,
public dialogRef: MatDialogRef<AdminSystemProfileFieldEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.isEditMode = !!data?.systemProfileField;
this.form = this.fb.group({
name: [{ value: data?.systemProfileField?.name || '', disabled: this.isEditMode }, Validators.required],
type: [data?.systemProfileField?.type || 'TEXT', Validators.required],
uniqueValue: [data?.systemProfileField?.uniqueValue || false]
});
}
ngOnInit(): void {}
onSubmit(): void {
if (this.form.valid) {
const systemProfileField = {
name: this.isEditMode ? this.data.systemProfileField.name : this.form.get('name')?.value,
type: this.form.get('type')?.value,
uniqueValue: this.form.get('uniqueValue')?.value
};
this.systemProfileFieldManagementService.update(systemProfileField).subscribe({
next: (result) => {
this.dialogRef.close(result);
},
error: (error) => {
console.error('Error saving system profile field:', error);
}
});
}
}
onCancel(): void {
this.dialogRef.close();
}
}
@@ -0,0 +1,50 @@
<header>
<h3>{{'admin.system_profile_fields' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary" (click)="createField()">
<mat-icon>add</mat-icon>
{{'admin.system_profile_fields.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_profile_fields.name' | i18n}}</th>
<td mat-cell *matCellDef="let field">{{field.name}}</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_profile_fields.type' | i18n}}</th>
<td mat-cell *matCellDef="let field">{{field.type}}</td>
</ng-container>
<ng-container matColumnDef="uniqueValue">
<th mat-header-cell *matHeaderCellDef>{{'admin.system_profile_fields.uniqueValue' | i18n}}</th>
<td mat-cell *matCellDef="let field">{{field.uniqueValue ? 'Yes' : 'No'}}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let field">
<button mat-icon-button (click)="editField(field)" [title]="'admin.system_profile_fields.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteField(field)" [title]="'admin.system_profile_fields.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,109 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from 'src/app/services/i18n.service';
import { SystemProfileFieldManagementService } from 'src/app/services/admin/systemprofilefield.management.service';
import { ConfirmDialog } from 'src/app/ui/confirm/confirm.component';
import { AdminSystemProfileFieldEditDialog } from './system-profile-field.edit';
@Component({
standalone: false,
selector: 'app-admin-system-profile-fields',
templateUrl: './system-profile-fields.component.html',
styleUrls: ['../admin.scss']
})
export class AdminSystemProfileFieldsComponent implements OnInit {
displayedColumns: string[] = ['name', 'type', 'uniqueValue', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(
private systemProfileFieldManagementService: SystemProfileFieldManagementService,
private i18n: I18nService,
private dialog: MatDialog
) {
this.dataSource = new MatTableDataSource();
}
ngOnInit(): void {
this.loadFields();
}
loadFields(): void {
this.systemProfileFieldManagementService.getSystemProfileFields(this.pageIndex, this.pageSize).subscribe(
(data: any) => {
this.dataSource.data = data.content || data;
this.totalElements = data.totalElements || data.length;
},
error => {
console.error('Error loading system profile fields:', error);
}
);
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadFields();
}
onSortChange(sort: any): void {
// Sorting can be implemented when backend supports it
console.log('Sort change:', sort);
}
deleteField(field: any): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.system_profile_fields.confirm_delete',
'args': [field.name]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.systemProfileFieldManagementService.deleteByName(field.name).subscribe(
() => {
this.loadFields();
},
error => {
console.error('Error deleting field:', error);
}
);
}
});
}
createField(): void {
const dialogRef = this.dialog.open(AdminSystemProfileFieldEditDialog, {
width: '600px',
data: {}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadFields();
}
});
}
editField(field: any): void {
const dialogRef = this.dialog.open(AdminSystemProfileFieldEditDialog, {
width: '600px',
data: { systemProfileField: field }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadFields();
}
});
}
}
@@ -0,0 +1,48 @@
<header>
<h3>{{'admin.system_properties' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary" (click)="createProperty()">
<mat-icon>add</mat-icon>
{{'admin.system_properties.create' | i18n}}
</button>
<button mat-raised-button color="primary" (click)="updatePretixClient()" style="margin-left: 8px;">
{{'admin.system_properties.update_pretix' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="key">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_properties.key' | i18n}}</th>
<td mat-cell *matCellDef="let property">{{property.key}}</td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_properties.value' | i18n}}</th>
<td mat-cell *matCellDef="let property">{{property.value}}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let property">
<button mat-icon-button (click)="editProperty(property)" [title]="'admin.system_properties.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteProperty(property)" [title]="'admin.system_properties.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,120 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from 'src/app/services/i18n.service';
import { SystemManagementService } from 'src/app/services/admin/system.management.service';
import { ConfirmDialog } from 'src/app/ui/confirm/confirm.component';
import { AdminSystemPropertyEditDialog } from './system-property.edit';
@Component({
standalone: false,
selector: 'app-admin-system-properties',
templateUrl: './system-properties.component.html',
styleUrls: ['../admin.scss']
})
export class AdminSystemPropertiesComponent implements OnInit {
displayedColumns: string[] = ['key', 'value', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(
private systemManagementService: SystemManagementService,
private i18n: I18nService,
private dialog: MatDialog
) {
this.dataSource = new MatTableDataSource();
}
ngOnInit(): void {
this.loadProperties();
}
loadProperties(): void {
this.systemManagementService.getProperties(this.pageIndex, this.pageSize).subscribe(
(data: any) => {
this.dataSource.data = data.content || data;
this.totalElements = data.totalElements || data.length;
},
error => {
console.error('Error loading system properties:', error);
}
);
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadProperties();
}
onSortChange(sort: any): void {
// Sorting can be implemented when backend supports it
console.log('Sort change:', sort);
}
deleteProperty(property: any): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.system_properties.confirm_delete',
'args': [property.key]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.systemManagementService.deleteProperty(property.key).subscribe(
() => {
this.loadProperties();
},
error => {
console.error('Error deleting property:', error);
}
);
}
});
}
updatePretixClient(): void {
this.systemManagementService.updatePretixClient().subscribe(
() => {
console.log('Pretix client updated successfully');
},
error => {
console.error('Error updating Pretix client:', error);
}
);
}
createProperty(): void {
const dialogRef = this.dialog.open(AdminSystemPropertyEditDialog, {
width: '600px',
data: {}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadProperties();
}
});
}
editProperty(property: any): void {
const dialogRef = this.dialog.open(AdminSystemPropertyEditDialog, {
width: '600px',
data: { systemProperty: property }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadProperties();
}
});
}
}
@@ -0,0 +1,35 @@
<h2 mat-dialog-title>{{ (isEditMode ? 'admin.system_properties.edit' : 'admin.system_properties.create') | i18n }}</h2>
<mat-dialog-content>
<form [formGroup]="form">
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.system_properties.key' | i18n }}</mat-label>
<input matInput formControlName="key" [placeholder]="'admin.system_properties.key' | i18n" required>
@if (form.get('key')?.hasError('required')) {
<mat-error>
{{ 'admin.system_properties.key_required' | i18n }}
</mat-error>
}
@if (isEditMode) {
<mat-hint>{{ 'admin.system_properties.key_readonly' | i18n }}</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.system_properties.value' | i18n }}</mat-label>
<textarea matInput formControlName="value" [placeholder]="'admin.system_properties.value' | i18n" rows="5" required></textarea>
@if (form.get('value')?.hasError('required')) {
<mat-error>
{{ 'admin.system_properties.value_required' | i18n }}
</mat-error>
}
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">{{ 'admin.cancel' | i18n }}</button>
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!form.valid">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,5 @@
/* System Property specific styles */
textarea {
font-family: monospace;
}
@@ -0,0 +1,53 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { SystemManagementService } from '../../../services/admin/system.management.service';
@Component({
standalone: false,
selector: 'admin-system-property-edit-dialog',
templateUrl: './system-property.edit.html',
styleUrls: ['../admin.scss', './system-property.edit.scss']
})
export class AdminSystemPropertyEditDialog implements OnInit {
form: FormGroup;
isEditMode: boolean = false;
constructor(
private fb: FormBuilder,
private systemManagementService: SystemManagementService,
public dialogRef: MatDialogRef<AdminSystemPropertyEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.isEditMode = !!data?.systemProperty;
this.form = this.fb.group({
key: [{ value: data?.systemProperty?.key || '', disabled: this.isEditMode }, Validators.required],
value: [data?.systemProperty?.value || '', Validators.required]
});
}
ngOnInit(): void {}
onSubmit(): void {
if (this.form.valid) {
const systemProperty = {
key: this.isEditMode ? this.data.systemProperty.key : this.form.get('key')?.value,
value: this.form.get('value')?.value
};
this.systemManagementService.createOrUpdate(systemProperty).subscribe({
next: (result) => {
this.dialogRef.close(result);
},
error: (error) => {
console.error('Error saving system property:', error);
}
});
}
}
onCancel(): void {
this.dialogRef.close();
}
}
@@ -0,0 +1,117 @@
<header>
<h3>{{'admin.timeslots' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary">
<mat-icon>add</mat-icon>
{{'admin.timeslots.create' | i18n}}
</button>
</header>
<mat-card>
<mat-card-content>
<form [formGroup]="filterForm" (ngSubmit)="applyFilter()">
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<mat-form-field appearance="outline">
<mat-label>{{'admin.timeslots.filter_owner' | i18n}}</mat-label>
<input matInput formControlName="owner" type="number">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.timeslots.filter_type' | i18n}}</mat-label>
<mat-select formControlName="type">
<mat-option value="">{{'admin.timeslots.all' | i18n}}</mat-option>
<mat-option value="AUDIO">AUDIO</mat-option>
<mat-option value="AUDIOSTREAM">AUDIOSTREAM</mat-option>
<mat-option value="VIDEO">VIDEO</mat-option>
<mat-option value="VIDEOSTREAM">VIDEOSTREAM</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.timeslots.filter_visibility' | i18n}}</mat-label>
<mat-select formControlName="visibility">
<mat-option value="">{{'admin.timeslots.all' | i18n}}</mat-option>
<mat-option value="PUBLIC">PUBLIC</mat-option>
<mat-option value="PROTECTED">PROTECTED</mat-option>
<mat-option value="PRIVATE">PRIVATE</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'admin.timeslots.search' | i18n}}</mat-label>
<input matInput formControlName="search">
</mat-form-field>
<button mat-raised-button color="primary" type="submit" style="align-self: center;">
<mat-icon>filter_list</mat-icon>
{{'admin.timeslots.apply_filter' | i18n}}
</button>
</div>
</form>
</mat-card-content>
</mat-card>
<br>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.timeslots.id' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.id}} </td>
</ng-container>
<ng-container matColumnDef="owner">
<th mat-header-cell *matHeaderCellDef> {{'admin.timeslots.owner' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.owner}} </td>
</ng-container>
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef> {{'admin.timeslots.title' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.title}} </td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{'admin.timeslots.type' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.type}} </td>
</ng-container>
<ng-container matColumnDef="visibility">
<th mat-header-cell *matHeaderCellDef> {{'admin.timeslots.visibility' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.visibility}} </td>
</ng-container>
<ng-container matColumnDef="start">
<th mat-header-cell *matHeaderCellDef> {{'admin.timeslots.start' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.start | date:'short'}} </td>
</ng-container>
<ng-container matColumnDef="end">
<th mat-header-cell *matHeaderCellDef> {{'admin.timeslots.end' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.end | date:'short'}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot">
<button mat-icon-button [title]="'admin.timeslots.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteTimeslot(timeslot.id)" [title]="'admin.timeslots.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,114 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { FormBuilder, FormGroup } from '@angular/forms';
import { I18nService } from './../../../services/i18n.service';
import { TimeslotManagementService } from '../../../services/admin/timeslot.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
@Component({
standalone: false,
selector: 'app-admin-timeslots',
templateUrl: './timeslots.component.html',
styleUrls: ['../admin.scss']
})
export class AdminTimeslotsComponent implements OnInit {
displayedColumns: string[] = ['id', 'owner', 'title', 'type', 'visibility', 'start', 'end', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
sortField = 'id';
sortDescending = false;
filterForm: FormGroup;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(
private timeslotManagementService: TimeslotManagementService,
private i18n: I18nService,
private formBuilder: FormBuilder,
public dialog: MatDialog
) {
this.dataSource = new MatTableDataSource<any>([]);
this.filterForm = this.formBuilder.group({
owner: [''],
type: [''],
visibility: [''],
search: ['']
});
}
ngOnInit(): void {
this.loadTimeslots();
}
loadTimeslots(): void {
const filters = this.filterForm.value;
this.timeslotManagementService.getTimeslots(
this.pageIndex,
this.pageSize,
this.sortField,
this.sortDescending,
filters.owner || undefined,
undefined,
undefined,
filters.type || undefined,
filters.visibility || undefined,
filters.search || undefined
).subscribe({
next: (data: any) => {
this.dataSource.data = data.content;
this.totalElements = data.totalElements;
},
error: (error) => {
console.error('Error loading timeslots:', error);
}
});
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadTimeslots();
}
onSortChange(sort: any): void {
this.sortField = sort.active || 'id';
this.sortDescending = sort.direction === 'desc';
this.loadTimeslots();
}
applyFilter(): void {
this.pageIndex = 0;
this.loadTimeslots();
}
deleteTimeslot(id: number, quota: boolean = false): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.timeslots.confirm_delete',
'args': [id.toString()]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.timeslotManagementService.deleteTimeslot(id, quota).subscribe({
next: () => {
this.loadTimeslots();
},
error: (error) => {
console.error('Error deleting timeslot:', error);
}
});
}
});
}
}
@@ -0,0 +1,49 @@
<h2 mat-dialog-title>{{ (isEditMode ? 'admin.user_aliases.edit' : 'admin.user_aliases.create') | i18n }}</h2>
<mat-dialog-content>
<form [formGroup]="form">
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.user_aliases.target' | i18n }}</mat-label>
<input matInput type="number" formControlName="target" [placeholder]="'admin.user_aliases.target' | i18n" required>
@if (form.get('target')?.hasError('required')) {
<mat-error>
{{ 'admin.user_aliases.target_required' | i18n }}
</mat-error>
}
<mat-hint>{{ 'admin.user_aliases.target_hint' | i18n }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.user_aliases.alias' | i18n }}</mat-label>
<input matInput formControlName="alias" [placeholder]="'admin.user_aliases.alias' | i18n" required>
@if (form.get('alias')?.hasError('required')) {
<mat-error>
{{ 'admin.user_aliases.alias_required' | i18n }}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.user_aliases.visibility' | i18n }}</mat-label>
<mat-select formControlName="visibility" required>
@for (visibility of visibilityOptions; track visibility) {
<mat-option [value]="visibility">
{{ visibility }}
</mat-option>
}
</mat-select>
@if (form.get('visibility')?.hasError('required')) {
<mat-error>
{{ 'admin.user_aliases.visibility_required' | i18n }}
</mat-error>
}
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">{{ 'admin.cancel' | i18n }}</button>
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!form.valid">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,53 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UserAliasManagementService } from '../../../services/admin/useralias.management.service';
@Component({
standalone: false,
selector: 'admin-user-alias-edit-dialog',
templateUrl: './user-alias.edit.html',
styleUrls: ['../admin.scss']
})
export class AdminUserAliasEditDialog implements OnInit {
form: FormGroup;
isEditMode: boolean = false;
visibilityOptions = ['PRIVATE', 'PROTECTED', 'PUBLIC'];
constructor(
private fb: FormBuilder,
private userAliasManagementService: UserAliasManagementService,
public dialogRef: MatDialogRef<AdminUserAliasEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.isEditMode = !!data?.userAlias;
this.form = this.fb.group({
id: [data?.userAlias?.id || null],
target: [data?.userAlias?.target || null, Validators.required],
alias: [data?.userAlias?.alias || '', Validators.required],
visibility: [data?.userAlias?.visibility || 'PROTECTED', Validators.required]
});
}
ngOnInit(): void {}
onSubmit(): void {
if (this.form.valid) {
const userAlias = this.form.value;
this.userAliasManagementService.createOrUpdateAlias(userAlias).subscribe({
next: (result) => {
this.dialogRef.close(result);
},
error: (error) => {
console.error('Error saving user alias:', error);
}
});
}
}
onCancel(): void {
this.dialogRef.close();
}
}
@@ -0,0 +1,55 @@
<header>
<h3>{{'admin.user_aliases' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary" (click)="createAlias()">
<mat-icon>add</mat-icon>
{{'admin.user_aliases.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.id' | i18n}}</th>
<td mat-cell *matCellDef="let alias">{{alias.id}}</td>
</ng-container>
<ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.alias' | i18n}}</th>
<td mat-cell *matCellDef="let alias">{{alias.alias}}</td>
</ng-container>
<ng-container matColumnDef="target">
<th mat-header-cell *matHeaderCellDef>{{'admin.user_aliases.target' | i18n}}</th>
<td mat-cell *matCellDef="let alias">{{alias.target}}</td>
</ng-container>
<ng-container matColumnDef="visibility">
<th mat-header-cell *matHeaderCellDef>{{'admin.user_aliases.visibility' | i18n}}</th>
<td mat-cell *matCellDef="let alias">{{alias.visibility}}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let alias">
<button mat-icon-button (click)="editAlias(alias)" [title]="'admin.user_aliases.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteAlias(alias)" [title]="'admin.user_aliases.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,109 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from 'src/app/services/i18n.service';
import { UserAliasManagementService } from 'src/app/services/admin/useralias.management.service';
import { ConfirmDialog } from 'src/app/ui/confirm/confirm.component';
import { AdminUserAliasEditDialog } from './user-alias.edit';
@Component({
standalone: false,
selector: 'app-admin-user-aliases',
templateUrl: './user-aliases.component.html',
styleUrls: ['../admin.scss']
})
export class AdminUserAliasesComponent implements OnInit {
displayedColumns: string[] = ['id', 'alias', 'target', 'visibility', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(
private userAliasManagementService: UserAliasManagementService,
private i18n: I18nService,
private dialog: MatDialog
) {
this.dataSource = new MatTableDataSource();
}
ngOnInit(): void {
this.loadAliases();
}
loadAliases(): void {
this.userAliasManagementService.getAliases(this.pageIndex, this.pageSize).subscribe(
(data: any) => {
this.dataSource.data = data.content || data;
this.totalElements = data.totalElements || data.length;
},
error => {
console.error('Error loading user aliases:', error);
}
);
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadAliases();
}
onSortChange(sort: any): void {
// Sorting can be implemented when backend supports it
console.log('Sort change:', sort);
}
deleteAlias(alias: any): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.user_aliases.confirm_delete',
'args': [alias.source]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.userAliasManagementService.deleteAlias(alias.id).subscribe(
() => {
this.loadAliases();
},
error => {
console.error('Error deleting alias:', error);
}
);
}
});
}
createAlias(): void {
const dialogRef = this.dialog.open(AdminUserAliasEditDialog, {
width: '600px',
data: {}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadAliases();
}
});
}
editAlias(alias: any): void {
const dialogRef = this.dialog.open(AdminUserAliasEditDialog, {
width: '600px',
data: { userAlias: alias }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadAliases();
}
});
}
}
+65
View File
@@ -0,0 +1,65 @@
<h2 mat-dialog-title>{{ (isEditMode ? 'admin.users.edit' : 'admin.users.create') | i18n }}</h2>
<mat-dialog-content>
<form [formGroup]="form">
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.users.username' | i18n }}</mat-label>
<input matInput formControlName="username" required>
<mat-error>
{{ 'admin.users.error.username' | i18n }}
</mat-error>
</mat-form-field>
@if (!isEditMode) {
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.users.password' | i18n }}</mat-label>
<input matInput type="password" formControlName="password" required>
<mat-error>
{{ 'admin.users.error.password' | i18n }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.users.password2' | i18n }}</mat-label>
<input matInput type="password" formControlName="password2" required>
@if (form.get('password2')?.hasError('required')) {
<mat-error>
{{ 'admin.users.error.password2' | i18n }}
</mat-error>
}
</mat-form-field>
@if (!passwordsMatch() && form.get('password')?.value && form.get('password2')?.value) {
<mat-error>
{{ 'admin.users.error.password_mismatch' | i18n }}
</mat-error>
}
}
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.users.status' | i18n }}</mat-label>
<mat-select formControlName="status" required>
@for (status of userStatuses; track status) {
<mat-option [value]="status">{{ 'admin.users.status.' + status | i18n }}</mat-option>
}
</mat-select>
<mat-error>
{{ 'admin.users.error.status' | i18n }}
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="disabled">
{{ 'admin.users.disabled' | i18n }}
</mat-checkbox>
<mat-checkbox formControlName="locked">
{{ 'admin.users.locked' | i18n }}
</mat-checkbox>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<a mat-button [mat-dialog-close]="false">{{ 'cancel' | i18n }}</a>
<a [disabled]="form.invalid || !passwordsMatch()" mat-raised-button (click)="save()" color="accent">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</a>
</mat-dialog-actions>
+89
View File
@@ -0,0 +1,89 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { UserManagementService } from '../../../services/admin/user.management.service';
import { I18nService } from './../../../services/i18n.service';
@Component({
standalone: false,
selector: 'app-admin-user-edit-dialog',
templateUrl: './user.edit.html',
styleUrls: ['../admin.scss']
})
export class AdminUserEditDialog implements OnInit {
form: FormGroup;
user: any;
isEditMode: boolean = false;
userStatuses = ['NORMAL', 'SLEEP', 'PURGE'];
constructor(
private userManagementService: UserManagementService,
private i18n: I18nService,
private formBuilder: FormBuilder,
public dialogRef: MatDialogRef<AdminUserEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.user = data ? JSON.parse(JSON.stringify(data)) : {};
this.isEditMode = !!this.user.id;
}
ngOnInit(): void {
const formConfig: any = {
username: [{ value: this.user.username || '', disabled: this.isEditMode }, Validators.required],
disabled: [this.user.disabled || false],
locked: [this.user.locked || false],
status: [this.user.status || 'NORMAL', Validators.required]
};
// Add password fields only for create mode
if (!this.isEditMode) {
formConfig.password = ['', Validators.required];
formConfig.password2 = ['', Validators.required];
}
this.form = this.formBuilder.group(formConfig);
}
passwordsMatch(): boolean {
if (this.isEditMode || !this.form) {
return true;
}
const password = this.form.get('password')?.value;
const password2 = this.form.get('password2')?.value;
return password === password2;
}
save(): void {
if (this.form.invalid || !this.passwordsMatch()) {
return;
}
const userData = {
...this.user,
username: this.form.value.username,
disabled: this.form.value.disabled,
locked: this.form.value.locked,
status: this.form.value.status
};
// Include password fields only for create mode
if (!this.isEditMode && this.form.value.password) {
userData.password = this.form.value.password;
userData.password2 = this.form.value.password2;
}
const saveObservable = this.isEditMode
? this.userManagementService.update(userData)
: this.userManagementService.create(userData);
saveObservable.subscribe({
next: (result) => {
this.dialogRef.close(result);
},
error: (error) => {
console.error('Error saving user:', error);
}
});
}
}
@@ -0,0 +1,62 @@
<header>
<h3>{{'admin.users' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary" (click)="createUser()">
<mat-icon>add</mat-icon>
{{'admin.users.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.users.id' | i18n}} </th>
<td mat-cell *matCellDef="let user"> {{user.id}} </td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef mat-sort-header="username"> {{'admin.users.username' | i18n}} </th>
<td mat-cell *matCellDef="let user"> {{user.username}} </td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header="status"> {{'admin.users.status' | i18n}} </th>
<td mat-cell *matCellDef="let user"> {{user.status}} </td>
</ng-container>
<ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef mat-sort-header="created"> {{'admin.users.created' | i18n}} </th>
<td mat-cell *matCellDef="let user"> {{user.created | date:'short'}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let user">
<button mat-icon-button (click)="editUser(user)" [title]="'admin.users.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button [routerLink]="['/admin/permissions']" [queryParams]="{username: user.username}" [title]="'admin.users.view_permissions' | i18n">
<mat-icon>security</mat-icon>
</button>
<button mat-icon-button [routerLink]="['/admin/quotas']" [queryParams]="{username: user.username}" [title]="'admin.users.view_quotas' | i18n">
<mat-icon>data_usage</mat-icon>
</button>
<button mat-icon-button (click)="deleteUser(user.username)" [title]="'admin.users.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,115 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from './../../../services/i18n.service';
import { UserManagementService } from '../../../services/admin/user.management.service';
import { ConfirmDialog } from './../../../ui/confirm/confirm.component';
import { AdminUserEditDialog } from './user.edit';
@Component({
standalone: false,
selector: 'app-admin-users',
templateUrl: './users.component.html',
styleUrls: ['../admin.scss']
})
export class AdminUsersComponent implements OnInit {
displayedColumns: string[] = ['id', 'username', 'status', 'created', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
sortField = 'username';
sortDescending = false;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(
private userManagementService: UserManagementService,
private i18n: I18nService,
public dialog: MatDialog
) {
this.dataSource = new MatTableDataSource<any>([]);
}
ngOnInit(): void {
this.loadUsers();
}
loadUsers(): void {
this.userManagementService.getUsers(this.pageIndex, this.pageSize, this.sortField, this.sortDescending)
.subscribe({
next: (data: any) => {
this.dataSource.data = data.content;
this.totalElements = data.totalElements;
},
error: (error) => {
console.error('Error loading users:', error);
}
});
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadUsers();
}
onSortChange(sort: any): void {
this.sortField = sort.active || 'username';
this.sortDescending = sort.direction === 'desc';
this.loadUsers();
}
createUser(): void {
const dialogRef = this.dialog.open(AdminUserEditDialog, {
data: null,
minWidth: '400px'
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadUsers();
}
});
}
editUser(user: any): void {
const dialogRef = this.dialog.open(AdminUserEditDialog, {
data: user,
minWidth: '400px'
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadUsers();
}
});
}
deleteUser(username: string): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.users.confirm_delete',
'args': [username]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.userManagementService.delete(username).subscribe({
next: () => {
this.loadUsers();
},
error: (error) => {
console.error('Error deleting user:', error);
}
});
}
});
}
}
@@ -0,0 +1,46 @@
<h2 mat-dialog-title>{{ (isEditMode ? 'admin.voucher_mappings.edit' : 'admin.voucher_mappings.create') | i18n }}</h2>
<mat-dialog-content>
<form [formGroup]="form">
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.voucher_mappings.name' | i18n }}</mat-label>
<input matInput formControlName="name" [placeholder]="'admin.voucher_mappings.name' | i18n" required>
@if (form.get('name')?.hasError('required')) {
<mat-error>
{{ 'admin.voucher_mappings.name_required' | i18n }}
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.voucher_mappings.quota' | i18n }}</mat-label>
<input matInput formControlName="quota" [placeholder]="'admin.voucher_mappings.quota' | i18n">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'admin.voucher_mappings.voucher' | i18n }}</mat-label>
<input matInput type="number" formControlName="voucher" [placeholder]="'admin.voucher_mappings.voucher' | i18n" required>
@if (form.get('voucher')?.hasError('required')) {
<mat-error>
{{ 'admin.voucher_mappings.voucher_required' | i18n }}
</mat-error>
}
@if (form.get('voucher')?.hasError('min')) {
<mat-error>
{{ 'admin.voucher_mappings.voucher_min' | i18n }}
</mat-error>
}
</mat-form-field>
<mat-checkbox formControlName="free">
{{ 'admin.voucher_mappings.free' | i18n }}
</mat-checkbox>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">{{ 'admin.cancel' | i18n }}</button>
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!form.valid">
{{ (isEditMode ? 'admin.save' : 'admin.create') | i18n }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,64 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { VoucherMappingManagementService } from '../../../services/admin/vouchermapping.management.service';
@Component({
standalone: false,
selector: 'admin-voucher-mapping-edit-dialog',
templateUrl: './voucher-mapping.edit.html',
styleUrls: ['../admin.scss']
})
export class AdminVoucherMappingEditDialog implements OnInit {
form: FormGroup;
isEditMode: boolean = false;
constructor(
private fb: FormBuilder,
private voucherMappingManagementService: VoucherMappingManagementService,
public dialogRef: MatDialogRef<AdminVoucherMappingEditDialog>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.isEditMode = !!data?.voucherMapping;
this.form = this.fb.group({
id: [data?.voucherMapping?.id || null],
name: [data?.voucherMapping?.name || '', Validators.required],
quota: [data?.voucherMapping?.quota || ''],
voucher: [data?.voucherMapping?.voucher || 0, [Validators.required, Validators.min(0)]],
free: [data?.voucherMapping?.free || false]
});
}
ngOnInit(): void {}
onSubmit(): void {
if (this.form.valid) {
const voucherMapping = this.form.value;
if (this.isEditMode) {
this.voucherMappingManagementService.update(voucherMapping).subscribe({
next: (result) => {
this.dialogRef.close(result);
},
error: (error) => {
console.error('Error updating voucher mapping:', error);
}
});
} else {
this.voucherMappingManagementService.create(voucherMapping).subscribe({
next: (result) => {
this.dialogRef.close(result);
},
error: (error) => {
console.error('Error creating voucher mapping:', error);
}
});
}
}
}
onCancel(): void {
this.dialogRef.close();
}
}
@@ -0,0 +1,60 @@
<header>
<h3>{{'admin.voucher_mappings' | i18n}}</h3>
<span class="spacer"></span>
<button mat-raised-button color="primary" (click)="createMapping()">
<mat-icon>add</mat-icon>
{{'admin.voucher_mappings.create' | i18n}}
</button>
</header>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.voucher_mappings.id' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.id}}</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.voucher_mappings.name' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.name}}</td>
</ng-container>
<ng-container matColumnDef="voucher">
<th mat-header-cell *matHeaderCellDef>{{'admin.voucher_mappings.voucher' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.voucher}}</td>
</ng-container>
<ng-container matColumnDef="quota">
<th mat-header-cell *matHeaderCellDef>{{'admin.voucher_mappings.quota' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.quota}}</td>
</ng-container>
<ng-container matColumnDef="free">
<th mat-header-cell *matHeaderCellDef>{{'admin.voucher_mappings.free' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.free ? 'Yes' : 'No'}}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">
<button mat-icon-button (click)="editMapping(mapping)" [title]="'admin.voucher_mappings.edit' | i18n">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button (click)="deleteMapping(mapping)" [title]="'admin.voucher_mappings.delete' | i18n">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalElements"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50, 100]"
(page)="onPageChange($event)"
showFirstLastButtons>
</mat-paginator>
</div>
@@ -0,0 +1,109 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
import { I18nService } from 'src/app/services/i18n.service';
import { VoucherMappingManagementService } from 'src/app/services/admin/vouchermapping.management.service';
import { ConfirmDialog } from 'src/app/ui/confirm/confirm.component';
import { AdminVoucherMappingEditDialog } from './voucher-mapping.edit';
@Component({
standalone: false,
selector: 'app-admin-voucher-mappings',
templateUrl: './voucher-mappings.component.html',
styleUrls: ['../admin.scss']
})
export class AdminVoucherMappingsComponent implements OnInit {
displayedColumns: string[] = ['id', 'name', 'voucher', 'quota', 'free', 'actions'];
dataSource: MatTableDataSource<any>;
totalElements = 0;
pageSize = 10;
pageIndex = 0;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(
private voucherMappingManagementService: VoucherMappingManagementService,
private i18n: I18nService,
private dialog: MatDialog
) {
this.dataSource = new MatTableDataSource();
}
ngOnInit(): void {
this.loadMappings();
}
loadMappings(): void {
this.voucherMappingManagementService.getVoucherMappings(this.pageIndex, this.pageSize).subscribe(
(data: any) => {
this.dataSource.data = data.content || data;
this.totalElements = data.totalElements || data.length;
},
error => {
console.error('Error loading voucher mappings:', error);
}
);
}
onPageChange(event: PageEvent): void {
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.loadMappings();
}
onSortChange(sort: any): void {
// Sorting can be implemented when backend supports it
console.log('Sort change:', sort);
}
deleteMapping(mapping: any): void {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'admin.voucher_mappings.confirm_delete',
'args': [mapping.name]
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.voucherMappingManagementService.delete(mapping).subscribe(
() => {
this.loadMappings();
},
error => {
console.error('Error deleting mapping:', error);
}
);
}
});
}
createMapping(): void {
const dialogRef = this.dialog.open(AdminVoucherMappingEditDialog, {
width: '600px',
data: {}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadMappings();
}
});
}
editMapping(mapping: any): void {
const dialogRef = this.dialog.open(AdminVoucherMappingEditDialog, {
width: '600px',
data: { voucherMapping: mapping }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadMappings();
}
});
}
}
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class JitsiRoomManagementService {
constructor(private http: HttpClient) {
}
getJitsiRooms(page: number = 0, size: number = 10) {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get(environment.apiUrl + "/jitsi/rooms/manage", { params });
}
createOrUpdateJitsiRoom(jitsiRoom: any) {
return this.http.post(environment.apiUrl + "/jitsi/rooms/manage", jitsiRoom);
}
deleteJitsiRoom(id: number, quota: boolean = false) {
let params = new HttpParams().set('quota', quota.toString());
return this.http.delete(environment.apiUrl + "/jitsi/rooms/manage/" + id, { params });
}
deleteAll(owner: number, quota: boolean = false) {
let params = new HttpParams().set('quota', quota.toString());
return this.http.delete(environment.apiUrl + "/jitsi/rooms/manage/all/" + owner, { params });
}
}
@@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class JukeboxManagementService {
constructor(private http: HttpClient) {
}
getConfig() {
return this.http.get(environment.apiUrl + "/jukebox/manage/config");
}
setConfig(config: any) {
return this.http.post(environment.apiUrl + "/jukebox/manage/config", config);
}
setActive() {
return this.http.put(environment.apiUrl + "/jukebox/manage", null);
}
disable() {
return this.http.delete(environment.apiUrl + "/jukebox/manage");
}
getStatus() {
return this.http.get(environment.apiUrl + "/jukebox/manage");
}
search(query: string) {
let params = new HttpParams().set('query', query);
return this.http.get(environment.apiUrl + "/jukebox/manage/search", { params });
}
queueSong(songData: any) {
return this.http.post(environment.apiUrl + "/jukebox/manage/queue", songData);
}
}
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class MinetestAccountManagementService {
constructor(private http: HttpClient) {
}
getMinetestAccounts(page: number = 0, size: number = 10) {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get(environment.apiUrl + "/minetest/accounts/manage", { params });
}
createOrUpdateMinetestAccount(minetestAccount: any) {
return this.http.post(environment.apiUrl + "/minetest/accounts/manage", minetestAccount);
}
deleteMinetestAccount(name: string, quota: boolean = false) {
let params = new HttpParams().set('quota', quota.toString());
return this.http.delete(environment.apiUrl + "/minetest/accounts/manage/" + name, { params });
}
deleteAll(owner: number, quota: boolean = false) {
let params = new HttpParams().set('quota', quota.toString());
return this.http.delete(environment.apiUrl + "/minetest/accounts/manage/all/" + owner, { params });
}
}
@@ -0,0 +1,74 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class OidcClientManagementService {
private apiUrl = environment.apiUrl + '/oidc/clients';
constructor(private http: HttpClient) { }
/**
* Get OIDC clients with pagination
* @param page Page number (default: 0)
* @param size Page size (default: 10)
*/
getClients(page: number = 0, size: number = 10): Observable<any> {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<any>(this.apiUrl, { params });
}
/**
* Get a specific OIDC client by name
* @param name Client name
*/
get(name: string): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/${name}`);
}
/**
* Get a specific OIDC client by client ID
* @param clientId Client ID
*/
getByClientId(clientId: string): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/id/${clientId}`);
}
/**
* Create a new OIDC client
* @param oidcClientModel OIDC client model object
*/
create(oidcClientModel: any): Observable<any> {
return this.http.post<any>(this.apiUrl, oidcClientModel);
}
/**
* Update an OIDC client
* @param client OIDC client object with id
*/
update(client: any): Observable<any> {
return this.http.patch<any>(this.apiUrl, client);
}
/**
* Delete an OIDC client
* @param name Client name
*/
deleteClient(name: string): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${name}`);
}
/**
* Create a new secret for an OIDC client
* @param name Client name
*/
createNewSecret(name: string): Observable<any> {
return this.http.post<any>(`${this.apiUrl}/${name}/secret`, {});
}
}
@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class ParteyMapManagementService {
constructor(private http: HttpClient) {
}
getParteyMaps(page: number = 0, size: number = 10, sort: string = 'id', desc: boolean = false) {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString())
.set('sort', sort)
.set('desc', desc.toString());
return this.http.get(environment.apiUrl + "/partey/maps", { params });
}
createOrUpdateParteyMap(parteyMap: any) {
return this.http.post(environment.apiUrl + "/partey/maps", parteyMap);
}
deleteParteyMap(id: string) {
return this.http.delete(environment.apiUrl + "/partey/maps/" + id);
}
}
@@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class PermissionManagementService {
constructor(private http: HttpClient) {
}
getPermissionsForUser(username: string, sort: string = 'name', ignoreStart: boolean = true) {
let params = new HttpParams()
.set('sort', sort)
.set('ignoreStart', ignoreStart.toString());
return this.http.get(environment.apiUrl + "/permissions/manage/" + username, { params });
}
getAllPermissionsForUser(username: string, sort: string = 'name') {
let params = new HttpParams().set('sort', sort);
return this.http.get(environment.apiUrl + "/permissions/manage/" + username + "/all", { params });
}
createPermission(permission: any) {
return this.http.post(environment.apiUrl + "/permissions/manage", permission);
}
createMultiple(permissionList: any[]) {
return this.http.post(environment.apiUrl + "/permissions/manage/multiple", permissionList);
}
updatePermission(permission: any) {
return this.http.patch(environment.apiUrl + "/permissions/manage", permission);
}
deletePermission(permission: any) {
return this.http.delete(environment.apiUrl + "/permissions/manage", { body: permission });
}
}
@@ -0,0 +1,66 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class PermissionMappingManagementService {
private apiUrl = environment.apiUrl + '/permissions/mappings';
constructor(private http: HttpClient) { }
/**
* Get permission mappings with pagination
* @param page Page number (default: 0)
* @param size Page size (default: 10)
*/
getPermissionMappings(page: number = 0, size: number = 10): Observable<any> {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<any>(this.apiUrl, { params });
}
/**
* Create a new permission mapping
* @param permissionMapping Permission mapping object
*/
create(permissionMapping: any): Observable<any> {
return this.http.post<any>(this.apiUrl, permissionMapping);
}
/**
* Create multiple permission mappings
* @param permissionMappings Array of permission mapping objects
*/
createList(permissionMappings: any[]): Observable<any[]> {
return this.http.post<any[]>(`${this.apiUrl}/list`, permissionMappings);
}
/**
* Update a permission mapping
* @param permissionMapping Permission mapping object with id
*/
update(permissionMapping: any): Observable<any> {
return this.http.patch<any>(this.apiUrl, permissionMapping);
}
/**
* Update multiple permission mappings
* @param permissionMappings Array of permission mapping objects with ids
*/
updateList(permissionMappings: any[]): Observable<any[]> {
return this.http.patch<any[]>(`${this.apiUrl}/list`, permissionMappings);
}
/**
* Delete a permission mapping
* @param permissionMapping Permission mapping object with id
*/
delete(permissionMapping: any): Observable<void> {
return this.http.request<void>('delete', this.apiUrl, { body: permissionMapping });
}
}
@@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class QuotaManagementService {
constructor(private http: HttpClient) {
}
getQuotasForUser(username: string, sort: string = 'name') {
let params = new HttpParams().set('sort', sort);
return this.http.get(environment.apiUrl + "/quotas/manage/" + username, { params });
}
getAllQuotasForUser(username: string, sort: string = 'name') {
let params = new HttpParams().set('sort', sort);
return this.http.get(environment.apiUrl + "/quotas/manage/" + username + "/all", { params });
}
getQuotasByName(name: string, sort: string = 'name') {
let params = new HttpParams().set('sort', sort);
return this.http.get(environment.apiUrl + "/quotas/manage/byname/" + name, { params });
}
createQuota(quota: any) {
return this.http.post(environment.apiUrl + "/quotas/manage", quota);
}
createMultiple(quotaList: any[]) {
return this.http.post(environment.apiUrl + "/quotas/manage/multiple", quotaList);
}
updateQuota(quota: any) {
return this.http.patch(environment.apiUrl + "/quotas/manage", quota);
}
deleteQuota(quota: any) {
return this.http.delete(environment.apiUrl + "/quotas/manage", { body: quota });
}
}
@@ -0,0 +1,58 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class QuotaMappingManagementService {
private apiUrl = environment.apiUrl + '/quotas/mappings';
constructor(private http: HttpClient) { }
/**
* Get quota mappings with pagination
* @param page Page number (default: 0)
* @param size Page size (default: 10)
*/
getQuotaMappings(page: number = 0, size: number = 10): Observable<any> {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<any>(this.apiUrl, { params });
}
/**
* Create a new quota mapping
* @param quotaMapping Quota mapping object
*/
create(quotaMapping: any): Observable<any> {
return this.http.post<any>(this.apiUrl, quotaMapping);
}
/**
* Create multiple quota mappings
* @param quotaMappings Array of quota mapping objects
*/
createList(quotaMappings: any[]): Observable<any[]> {
return this.http.post<any[]>(`${this.apiUrl}/list`, quotaMappings);
}
/**
* Update a quota mapping
* @param quotaMapping Quota mapping object with id
*/
update(quotaMapping: any): Observable<any> {
return this.http.patch<any>(this.apiUrl, quotaMapping);
}
/**
* Delete a quota mapping
* @param quotaMapping Quota mapping object with id
*/
delete(quotaMapping: any): Observable<void> {
return this.http.request<void>('delete', this.apiUrl, { body: quotaMapping });
}
}
@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class ServiceManagementService {
constructor(private http: HttpClient) {
}
getAllServices(page: number = 0, size: number = 10) {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get(environment.apiUrl + "/services/manage", { params });
}
createOrUpdateService(service: any) {
return this.http.post(environment.apiUrl + "/services/manage", service);
}
deleteService(service: any) {
return this.http.delete(environment.apiUrl + "/services/manage", { body: service });
}
}
@@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class ShortenedUrlManagementService {
constructor(private http: HttpClient) {
}
getShortenedUrls(page: number = 0, size: number = 10, search: string = '') {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString())
.set('search', search);
return this.http.get(environment.apiUrl + "/url/shortener/manage", { params });
}
createOrUpdateShortenedUrl(shortenedUrl: any) {
return this.http.post(environment.apiUrl + "/url/shortener/manage", shortenedUrl);
}
deleteShortenedUrl(code: string, quota: boolean = false) {
let params = new HttpParams().set('quota', quota.toString());
return this.http.delete(environment.apiUrl + "/url/shortener/manage/" + code, { params });
}
deleteAll(owner: number, quota: boolean = false) {
let params = new HttpParams().set('quota', quota.toString());
return this.http.delete(environment.apiUrl + "/url/shortener/manage/all/" + owner, { params });
}
}
@@ -0,0 +1,65 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class SystemManagementService {
private apiUrl = environment.apiUrl + '/system';
constructor(private http: HttpClient) { }
/**
* Update Pretix client configuration
*/
updatePretixClient(): Observable<void> {
return this.http.post<void>(`${this.apiUrl}/pretix`, {});
}
/**
* Get system properties with pagination
* @param page Page number (default: 0)
* @param size Page size (default: 10)
*/
getProperties(page: number = 0, size: number = 10): Observable<any[]> {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<any[]>(`${this.apiUrl}/properties`, { params });
}
/**
* Get a specific system property by key
* @param key Property key
*/
getProperty(key: string): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/properties/${key}`);
}
/**
* Create or update a system property
* @param systemProperty System property object
*/
createOrUpdate(systemProperty: any): Observable<any> {
return this.http.post<any>(`${this.apiUrl}/properties`, systemProperty);
}
/**
* Create or update multiple system properties
* @param systemProperties Array of system property objects
*/
createOrUpdateList(systemProperties: any[]): Observable<any[]> {
return this.http.post<any[]>(`${this.apiUrl}/properties/list`, systemProperties);
}
/**
* Delete a system property
* @param key Property key
*/
deleteProperty(key: string): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/properties/${key}`);
}
}
@@ -0,0 +1,58 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class SystemProfileFieldManagementService {
private apiUrl = environment.apiUrl + '/profiles/system';
constructor(private http: HttpClient) { }
/**
* Get system profile fields with pagination
* @param page Page number (default: 0)
* @param size Page size (default: 10)
*/
getSystemProfileFields(page: number = 0, size: number = 10): Observable<any> {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<any>(this.apiUrl, { params });
}
/**
* Get a specific system profile field by name
* @param name Field name
*/
getByName(name: string): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/${name}`);
}
/**
* Update a system profile field
* @param systemProfileField System profile field object
*/
update(systemProfileField: any): Observable<any> {
return this.http.post<any>(this.apiUrl, systemProfileField);
}
/**
* Update multiple system profile fields
* @param systemProfileFields Array of system profile field objects
*/
updateList(systemProfileFields: any[]): Observable<any[]> {
return this.http.post<any[]>(`${this.apiUrl}/list`, systemProfileFields);
}
/**
* Delete a system profile field
* @param name Field name
*/
deleteByName(name: string): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${name}`);
}
}
@@ -0,0 +1,45 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class TimeslotManagementService {
constructor(private http: HttpClient) {
}
getTimeslots(page: number = 0, size: number = 10, sort: string = 'id', desc: boolean = false,
owner?: number, ownerInvert?: boolean, after?: string, type?: string,
visibility?: string, search?: string) {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString())
.set('sort', sort)
.set('desc', desc.toString());
if (owner !== undefined) params = params.set('owner', owner.toString());
if (ownerInvert !== undefined) params = params.set('owner-invert', ownerInvert.toString());
if (after) params = params.set('after', after);
if (type) params = params.set('type', type);
if (visibility) params = params.set('visibility', visibility);
if (search) params = params.set('search', search);
return this.http.get(environment.apiUrl + "/partey/timeslots/manage", { params });
}
createOrUpdateTimeslot(timeslot: any) {
return this.http.post(environment.apiUrl + "/partey/timeslots/manage", timeslot);
}
deleteTimeslot(id: number, quota: boolean = false) {
let params = new HttpParams().set('quota', quota.toString());
return this.http.delete(environment.apiUrl + "/partey/timeslots/manage/" + id, { params });
}
deleteAll(owner: number, quota: boolean = false) {
let params = new HttpParams().set('quota', quota.toString());
return this.http.delete(environment.apiUrl + "/partey/timeslots/manage/all/" + owner, { params });
}
}
@@ -0,0 +1,37 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class UserManagementService {
constructor(private http: HttpClient) {
}
getUsers(page: number = 0, size: number = 10, sort: string = 'username', descending: boolean = false) {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString())
.set('sort', sort)
.set('descending', descending.toString());
return this.http.get(environment.apiUrl + "/users/manage", { params });
}
getUserByUsername(username: string) {
return this.http.get(environment.apiUrl + "/users/manage/" + username);
}
create(userModel: any) {
return this.http.post(environment.apiUrl + "/users/manage", userModel);
}
update(userModel: any) {
return this.http.patch(environment.apiUrl + "/users/manage", userModel);
}
delete(username: string) {
return this.http.delete(environment.apiUrl + "/users/manage/" + username);
}
}
@@ -0,0 +1,50 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class UserAliasManagementService {
private apiUrl = environment.apiUrl + '/users/aliases/manage';
constructor(private http: HttpClient) { }
/**
* Get all user aliases with pagination
* @param page Page number (default: 0)
* @param size Page size (default: 10)
*/
getAliases(page: number = 0, size: number = 10): Observable<any> {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<any>(this.apiUrl, { params });
}
/**
* Get aliases for a specific user
* @param username Username
*/
getAliasesForUser(username: string): Observable<any[]> {
return this.http.get<any[]>(`${this.apiUrl}/${username}`);
}
/**
* Create or update a user alias
* @param userAlias User alias object
*/
createOrUpdateAlias(userAlias: any): Observable<any> {
return this.http.post<any>(this.apiUrl, userAlias);
}
/**
* Delete a user alias
* @param id Alias ID
*/
deleteAlias(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
@@ -0,0 +1,41 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class UserDataManagementService {
private apiUrl = environment.apiUrl + '/users/manage/data';
constructor(private http: HttpClient) { }
/**
* Get user data for a specific user
* @param username Username
*/
getForUser(username: string): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/${username}`);
}
/**
* Purge user data (all users)
* @param dry Dry run flag (true for simulation, false for actual purge)
*/
purge(dry: boolean): Observable<void> {
let params = new HttpParams().set('dry', dry.toString());
return this.http.post<void>(`${this.apiUrl}/purge`, {}, { params });
}
/**
* Purge user data for a specific user
* @param username Username
* @param dry Dry run flag (true for simulation, false for actual purge)
*/
purgeByUsername(username: string, dry: boolean): Observable<void> {
let params = new HttpParams().set('dry', dry.toString());
return this.http.post<void>(`${this.apiUrl}/purge/${username}`, {}, { params });
}
}
@@ -0,0 +1,37 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class UserReportManagementService {
constructor(private http: HttpClient) {
}
getParteyUserReports(page: number = 0, size: number = 10, sort: string = 'id', desc: boolean = false) {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString())
.set('sort', sort)
.set('desc', desc.toString());
return this.http.get(environment.apiUrl + "/partey/reports", { params });
}
createOrUpdateParteyUserReport(parteyUserReport: any) {
return this.http.post(environment.apiUrl + "/partey/reports", parteyUserReport);
}
deleteParteyUserReport(id: number) {
return this.http.delete(environment.apiUrl + "/partey/reports/" + id);
}
deleteAllParteyUserReports(before?: string) {
let params = new HttpParams();
if (before) {
params = params.set('before', before);
}
return this.http.delete(environment.apiUrl + "/partey/reports", { params });
}
}
@@ -0,0 +1,45 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class UserTagManagementService {
constructor(private http: HttpClient) {
}
getParteyUserTags(page: number = 0, size: number = 10, sort: string = 'target', desc: boolean = false) {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString())
.set('sort', sort)
.set('desc', desc.toString());
return this.http.get(environment.apiUrl + "/partey/tags/manage", { params });
}
getParteyUserTagsForTarget(username: string) {
return this.http.get(environment.apiUrl + "/partey/tags/manage/" + username);
}
getNonExpiredParteyUserTagsForTarget(username: string) {
return this.http.get(environment.apiUrl + "/partey/tags/manage/" + username + "/active");
}
createParteyUserTag(parteyUserTag: any) {
return this.http.post(environment.apiUrl + "/partey/tags/manage", parteyUserTag);
}
createMultiple(parteyUserTagList: any[]) {
return this.http.post(environment.apiUrl + "/partey/tags/manage/multiple", parteyUserTagList);
}
updateParteyUserTag(parteyUserTag: any) {
return this.http.patch(environment.apiUrl + "/partey/tags/manage", parteyUserTag);
}
deleteParteyUserTag(id: number) {
return this.http.delete(environment.apiUrl + "/partey/tags/manage/" + id);
}
}
@@ -0,0 +1,58 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class VoucherMappingManagementService {
private apiUrl = environment.apiUrl + '/vouchers/mappings';
constructor(private http: HttpClient) { }
/**
* Get voucher mappings with pagination
* @param page Page number (default: 0)
* @param size Page size (default: 10)
*/
getVoucherMappings(page: number = 0, size: number = 10): Observable<any> {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<any>(this.apiUrl, { params });
}
/**
* Create a new voucher mapping
* @param voucherMapping Voucher mapping object
*/
create(voucherMapping: any): Observable<any> {
return this.http.post<any>(this.apiUrl, voucherMapping);
}
/**
* Create multiple voucher mappings
* @param voucherMappings Array of voucher mapping objects
*/
createList(voucherMappings: any[]): Observable<any[]> {
return this.http.post<any[]>(`${this.apiUrl}/list`, voucherMappings);
}
/**
* Update a voucher mapping
* @param voucherMapping Voucher mapping object with id
*/
update(voucherMapping: any): Observable<any> {
return this.http.patch<any>(this.apiUrl, voucherMapping);
}
/**
* Delete a voucher mapping
* @param voucherMapping Voucher mapping object with id
*/
delete(voucherMapping: any): Observable<void> {
return this.http.request<void>('delete', this.apiUrl, { body: voucherMapping });
}
}
+5
View File
@@ -64,6 +64,11 @@
<mat-icon>widgets</mat-icon> {{'services' | i18n}}
</a>
}
@if (auth && auth.authenticated && hasAdminRole()) {
<a routerLink="/admin" routerLinkActive="active" mat-list-item>
<mat-icon>admin_panel_settings</mat-icon> {{'admin.title' | i18n}}
</a>
}
<a routerLink="/tokens" mat-list-item>
<mat-icon>card_giftcard</mat-icon> {{'tokens.redeem' | i18n}}
</a>
+7
View File
@@ -158,6 +158,13 @@ export class MainComponent {
}
}
hasAdminRole(): boolean {
if (this.auth && this.auth.authorities) {
return this.auth.authorities.some((auth: any) => auth.authority === 'ROLE_ADMIN');
}
return false;
}
openExternal(url, target = '_self') {
window.open(url, target);
}
+420 -12
View File
@@ -5,6 +5,375 @@
".": "Erweitert"
}
},
"admin": {
".": "Administration",
"actions": "Aktionen",
"cancel": "Abbrechen",
"jitsi_rooms": {
".": "Jitsi-Räume",
"confirm_delete": "Bist du sicher, dass du den Raum '{0}' löschen möchtest?",
"create": "Jitsi-Raum erstellen",
"delete": "Jitsi-Raum löschen",
"edit": "Jitsi-Raum bearbeiten",
"expires": "Läuft ab",
"id": "ID",
"moderation_starts": "Moderation beginnt",
"moderator": "Moderator",
"name": "Name",
"owner": "Besitzer",
"owner_hint": "Benutzer-ID des Raumbesitzers",
"room": "Raum",
"room_required": "Raum ist erforderlich",
"starts": "Beginnt",
"subject": "Betreff",
"title": "Jitsi-Räume"
},
"jukebox": {
".": "Jukebox",
"activate": "Jukebox aktivieren",
"autoplay": "Automatische Wiedergabe",
"channel": "Kanal",
"configuration": "Konfiguration",
"disable": "Jukebox deaktivieren",
"error": {
"channel": "Kanal ist erforderlich",
"max_queue_size": "Muss eine positive Zahl sein",
"max_search_results": "Muss eine positive Zahl sein"
},
"max_queue_size": "Maximale Warteschlangengröße",
"max_search_results": "Maximale Suchergebnisse",
"save_config": "Konfiguration speichern",
"status": "Status"
},
"minetest_accounts": {
".": "Minetest-Accounts",
"confirm_delete": "Bist du sicher, dass du den Minetest-Account '{0}' löschen möchtest?",
"create": "Minetest-Account erstellen",
"created": "Erstellt",
"delete": "Minetest-Account löschen",
"edit": "Minetest-Account bearbeiten",
"name": "Name",
"owner": "Besitzer"
},
"oidc_clients": {
".": "OIDC-Clients",
"advanced_settings": "Erweiterte Einstellungen",
"alias_allowed": "Alias erlaubt",
"alias_quota": "Alias-Kontingent",
"alias_subject": "Alias-Betreff",
"always_permitted": "Immer erlaubt",
"auth_methods": "Authentifizierungsmethoden",
"authorize": "Autorisierung erforderlich",
"backchannel_logout_session_required": "Backchannel-Logout-Session erforderlich",
"backchannel_logout_uri": "Backchannel-Logout-URI",
"category": "Kategorie",
"client_id": "Client-ID",
"client_name": "Client-Name",
"client_name_required": "Client-Name ist erforderlich",
"client_secret": "Client-Secret",
"client_id_copied": "Client-ID in Zwischenablage kopiert",
"confirm_delete": "Bist du sicher, dass du den OIDC-Client '{0}' löschen möchtest?",
"confirm_new_secret": "Bist du sicher, dass du ein neues Geheimnis für Client '{0}' generieren möchtest?",
"copy_client_id": "Client-ID in Zwischenablage kopieren",
"copy_secret": "Secret in Zwischenablage kopieren",
"create": "OIDC-Client erstellen",
"create_client": "OIDC-Client erstellen",
"hide_secret": "Secret verbergen",
"secret_copied": "Client-Secret in Zwischenablage kopiert",
"show_secret": "Secret anzeigen",
"delete": "OIDC-Client löschen",
"edit": "OIDC-Client bearbeiten",
"edit_client": "OIDC-Client bearbeiten",
"frontchannel_logout_session_required": "Frontchannel-Logout-Session erforderlich",
"frontchannel_logout_uri": "Frontchannel-Logout-URI",
"grant_types": "Grant-Typen",
"id": "ID",
"login_url": "Login-URL",
"logout_settings": "Logout-Einstellungen",
"new_secret": "Neues Geheimnis generieren",
"redirect_uris": "Weiterleitungs-URIs",
"redirect_uris_hint": "Kommagetrennte Liste von Weiterleitungs-URIs",
"redirect_uris_required": "Mindestens eine Weiterleitungs-URI ist erforderlich",
"scopes": "Scopes",
"title": "OIDC-Clients",
"token_lifetime": "Token-Lebensdauer (Sekunden)",
"token_lifetime_hint": "Token-Lebensdauer in Sekunden"
},
"partey_maps": {
".": "Partey-Karten",
"confirm_delete": "Bist du sicher, dass du die Partey-Karte '{0}' löschen möchtest?",
"create": "Partey-Karte erstellen",
"delete": "Partey-Karte löschen",
"edit": "Partey-Karte bearbeiten",
"id": "ID",
"name": "Name",
"policy_type": "Richtlinientyp",
"tags": "Tags"
},
"partey_reports": {
".": "Partey-Meldungen",
"confirm_delete": "Bist du sicher, dass du diese Meldung löschen möchtest?",
"confirm_delete_all": "Bist du sicher, dass du alle Meldungen löschen möchtest?",
"created": "Erstellt",
"delete": "Meldung löschen",
"delete_all": "Alle löschen",
"id": "ID",
"reported": "Gemeldet",
"reporter": "Melder",
"view": "Meldung anzeigen",
"world": "Welt"
},
"partey_tags": {
".": "Partey-Tags",
"confirm_delete": "Bist du sicher, dass du den Partey-Tag '{0}' löschen möchtest?",
"create": "Partey-Tag erstellen",
"delete": "Partey-Tag löschen",
"edit": "Partey-Tag bearbeiten",
"expires": "Läuft ab",
"id": "ID",
"name": "Name",
"starts": "Beginnt",
"target": "Ziel"
},
"permission_mappings": {
".": "Berechtigungs-Zuordnungen",
"addon": "Addon",
"confirm_delete": "Bist du sicher, dass du die Berechtigungs-Zuordnung '{0}' löschen möchtest?",
"create": "Berechtigungs-Zuordnung erstellen",
"create_mapping": "Berechtigungs-Zuordnung erstellen",
"delete": "Berechtigungs-Zuordnung löschen",
"edit": "Berechtigungs-Zuordnung bearbeiten",
"edit_mapping": "Berechtigungs-Zuordnung bearbeiten",
"expires": "Läuft ab",
"expires_question": "Läuft ab-Frage",
"id": "ID",
"item": "Element",
"item_required": "Element ist erforderlich",
"lifetime": "Lebensdauer",
"lifetime_round": "Lebensdauer-Rundung",
"lifetime_unit": "Lebensdauer-Einheit",
"names": "Namen",
"names_hint": "Kommagetrennte Liste von Berechtigungsnamen",
"names_required": "Mindestens ein Berechtigungsname ist erforderlich",
"product": "Produkt",
"starts": "Beginnt",
"starts_question": "Beginnt-Frage",
"title": "Berechtigungs-Zuordnungen"
},
"permissions": {
".": "Berechtigungen",
"addon": "Addon",
"confirm_delete": "Bist du sicher, dass du die Berechtigung '{0}' löschen möchtest?",
"create": "Berechtigung erstellen",
"delete": "Berechtigung löschen",
"edit": "Berechtigung bearbeiten",
"error": {
"expires": "Ungültiges Ablaufdatum",
"name": "Name ist erforderlich",
"starts": "Ungültiges Startdatum"
},
"expires": "Läuft ab",
"for_user": "Berechtigungen für {0}",
"id": "ID",
"name": "Name",
"no_permissions": "Keine Berechtigungen gefunden",
"search": "Suchen",
"search_username": "Nach Benutzername suchen",
"starts": "Beginnt"
},
"quota_mappings": {
".": "Kontingent-Zuordnungen",
"append": "Anhängen",
"confirm_delete": "Bist du sicher, dass du die Kontingent-Zuordnung '{0}' löschen möchtest?",
"create": "Kontingent-Zuordnung erstellen",
"create_mapping": "Kontingent-Zuordnung erstellen",
"delete": "Kontingent-Zuordnung löschen",
"disposable": "Einweg",
"edit": "Kontingent-Zuordnung bearbeiten",
"edit_mapping": "Kontingent-Zuordnung bearbeiten",
"id": "ID",
"items": "Elemente",
"items_hint": "Kommagetrennte Liste von Element-IDs",
"items_required": "Mindestens ein Element ist erforderlich",
"name": "Name",
"name_required": "Name ist erforderlich",
"products": "Produkte",
"products_hint": "Kommagetrennte Liste von Produktnamen",
"title": "Kontingent-Zuordnungen",
"unit": "Einheit",
"value": "Wert",
"value_min": "Wert muss mindestens 0 sein",
"value_required": "Wert ist erforderlich"
},
"quotas": {
".": "Kontingente",
"confirm_delete": "Bist du sicher, dass du das Kontingent '{0}' löschen möchtest?",
"create": "Kontingent erstellen",
"create_quota": "Kontingent erstellen",
"delete": "Kontingent löschen",
"disposable": "Einweg",
"edit": "Kontingent bearbeiten",
"edit_quota": "Kontingent bearbeiten",
"for_user": "Kontingente für {0}",
"id": "ID",
"name": "Name",
"name_required": "Name ist erforderlich",
"no_quotas": "Keine Kontingente gefunden",
"search": "Suchen",
"search_username": "Nach Benutzername suchen",
"unit": "Einheit",
"value": "Wert",
"value_min": "Wert muss mindestens 0 sein",
"value_required": "Wert ist erforderlich"
},
"create": "Erstellen",
"save": "Speichern",
"services": {
".": "Dienste",
"always_permitted": "Immer erlaubt",
"category": "Kategorie",
"confirm_delete": "Bist du sicher, dass du den Dienst '{0}' löschen möchtest?",
"create": "Dienst erstellen",
"create_service": "Dienst erstellen",
"delete": "Dienst löschen",
"edit": "Dienst bearbeiten",
"edit_service": "Dienst bearbeiten",
"name": "Name",
"name_required": "Name ist erforderlich",
"permission": "Berechtigung",
"same_site": "Gleiche Seite",
"url": "URL",
"url_required": "URL ist erforderlich"
},
"shortened_urls": {
".": "Kurz-URLs",
"code": "Code",
"confirm_delete": "Bist du sicher, dass du die Kurz-URL '{0}' löschen möchtest?",
"create": "Kurz-URL erstellen",
"created": "Erstellt",
"delete": "Kurz-URL löschen",
"edit": "Kurz-URL bearbeiten",
"owner": "Besitzer",
"search": "Suchen",
"search_placeholder": "Kurz-URLs durchsuchen...",
"url": "URL"
},
"system_profile_fields": {
".": "System-Profilfelder",
"confirm_delete": "Bist du sicher, dass du das Profilfeld '{0}' löschen möchtest?",
"create": "Profilfeld erstellen",
"delete": "Profilfeld löschen",
"edit": "Profilfeld bearbeiten",
"label": "Bezeichnung",
"name": "Name",
"name_readonly": "Name kann nach der Erstellung nicht geändert werden",
"name_required": "Name ist erforderlich",
"required": "Erforderlich",
"title": "System-Profilfelder",
"type": "Typ",
"type_required": "Typ ist erforderlich",
"unique_value": "Eindeutiger Wert",
"uniqueValue": "Eindeutiger Wert"
},
"system_properties": {
".": "System-Eigenschaften",
"confirm_delete": "Bist du sicher, dass du die Eigenschaft '{0}' löschen möchtest?",
"create": "Eigenschaft erstellen",
"delete": "Eigenschaft löschen",
"edit": "Eigenschaft bearbeiten",
"key": "Schlüssel",
"key_readonly": "Schlüssel kann nach der Erstellung nicht geändert werden",
"key_required": "Schlüssel ist erforderlich",
"title": "System-Eigenschaften",
"update_pretix": "Pretix-Client aktualisieren",
"value": "Wert",
"value_required": "Wert ist erforderlich"
},
"timeslots": {
".": "Zeitfenster",
"all": "Alle",
"apply_filter": "Filter anwenden",
"confirm_delete": "Bist du sicher, dass du das Zeitfenster '{0}' löschen möchtest?",
"create": "Zeitfenster erstellen",
"delete": "Zeitfenster löschen",
"edit": "Zeitfenster bearbeiten",
"end": "Ende",
"filter_owner": "Nach Besitzer filtern",
"filter_type": "Nach Typ filtern",
"filter_visibility": "Nach Sichtbarkeit filtern",
"id": "ID",
"owner": "Besitzer",
"search": "Suchen",
"start": "Start",
"title": "Titel",
"type": "Typ",
"visibility": "Sichtbarkeit"
},
"title": "Admin",
"user_aliases": {
".": "Benutzer-Aliase",
"alias": "Alias",
"alias_required": "Alias ist erforderlich",
"confirm_delete": "Bist du sicher, dass du den Alias '{0}' löschen möchtest?",
"create": "Benutzer-Alias erstellen",
"delete": "Benutzer-Alias löschen",
"edit": "Benutzer-Alias bearbeiten",
"id": "ID",
"source": "Quelle",
"target": "Ziel",
"target_hint": "Benutzer-ID des Zielbenutzers",
"target_required": "Ziel-Benutzer-ID ist erforderlich",
"title": "Benutzer-Aliase",
"visibility": "Sichtbarkeit",
"visibility_required": "Sichtbarkeit ist erforderlich"
},
"users": {
".": "Benutzer",
"confirm_delete": "Bist du sicher, dass du den Benutzer '{0}' löschen möchtest?",
"create": "Benutzer erstellen",
"created": "Erstellt",
"delete": "Benutzer löschen",
"disabled": "Deaktiviert",
"edit": "Benutzer bearbeiten",
"error": {
"password": "Passwort ist erforderlich",
"password2": "Passwortbestätigung ist erforderlich",
"password_mismatch": "Passwörter stimmen nicht überein",
"status": "Status ist erforderlich",
"username": "Benutzername ist erforderlich"
},
"id": "ID",
"locked": "Gesperrt",
"password": "Passwort",
"password2": "Passwort bestätigen",
"status": {
".": "Status",
"NORMAL": "Normal",
"PURGE": "Bereinigen",
"SLEEP": "Ruhezustand"
},
"username": "Benutzername",
"view": "Benutzer anzeigen",
"view_permissions": "Berechtigungen anzeigen",
"view_quotas": "Kontingente anzeigen"
},
"voucher_mappings": {
".": "Gutschein-Zuordnungen",
"confirm_delete": "Bist du sicher, dass du die Gutschein-Zuordnung '{0}' löschen möchtest?",
"create": "Gutschein-Zuordnung erstellen",
"delete": "Gutschein-Zuordnung löschen",
"edit": "Gutschein-Zuordnung bearbeiten",
"free": "Kostenlos",
"id": "ID",
"name": "Name",
"name_required": "Name ist erforderlich",
"quota": "Kontingent",
"title": "Gutschein-Zuordnungen",
"voucher": "Gutschein",
"voucher_min": "Gutschein muss mindestens 0 sein",
"voucher_required": "Gutschein ist erforderlich"
}
},
"borrow": {
".": "Ausleihen",
"items": {
@@ -17,8 +386,8 @@
"MANUAL": "Manuel",
"PERIOD": "Periodisch"
},
"create": "Neues Item erstellen",
"confirmDelete": "Bist du sicher, dass du das Item '{0}' löschen möchtest?",
"create": "Neues Item erstellen",
"delete": "Item löschen",
"description": "Beschreibung",
"edit": "Item bearbeiten",
@@ -28,9 +397,9 @@
"availability": "Bitte Verfügbarkeit auswählen.",
"description": "Bitte eine Beschreibung eintragen.",
"email": "Bitte eine gültige E-Mail Addresse eintragen.",
"name": "Bitte einen Namen eintragen.",
"maxDuration": "Max. Dauer muss größer als die min. Dauer sein.",
"minDuration": "Min. Dauer muss kleiner als die max. Dauer sein.",
"name": "Bitte einen Namen eintragen.",
"slot": {
"end": "Ende muss nach dem Beginn sein.",
"endDay": "Wochentag muss größer oder gleich dem Beginn sein.",
@@ -49,19 +418,21 @@
"search": "Suche",
"slot": {
".": "Slot",
"add": "Slot hinzufügen",
"addManual": "Manuellen Slot hinzufügen",
"addPeriod": "Periodischen Slot hinzufügen",
"day": {
".": "Tag",
"MONDAY": "Montag",
"TUESDAY": "Dienstag",
"WEDNESDAY": "Mittwoch",
"THURSDAY": "Donnerstag",
"FRIDAY": "Freitag",
"MONDAY": "Montag",
"SATURDAY": "Samstag",
"SUNDAY": "Sonntag"
"SUNDAY": "Sonntag",
"THURSDAY": "Donnerstag",
"TUESDAY": "Dienstag",
"WEDNESDAY": "Mittwoch"
},
"delete": "Slot entfernen",
"dublicate": "Slot duplizieren",
"end": "Ende",
"endDay": "Wochentag Ende",
"endTime": "Ende",
@@ -73,13 +444,36 @@
},
"url": "Url"
},
"item": {
"name": "Item-Name"
},
"proving": {
".": "Proving",
"camera": "Camera",
"flash": "Flash"
},
"request": {
"comment": "Kommentar",
"end": "Ende",
"ends": "Endet",
"error": {
"comment": "Bitte einen Kommentar eingeben.",
"end": "Ende muss nach dem Beginn sein.",
"start": "Beginn muss vor dem Ende sein."
},
"item": "Item",
"owner": "Besitzer",
"start": "Beginn",
"started": "Gestartet",
"user": "Benutzer"
},
"requests": {
".": "Requests"
".": "Anfragen",
"actions": "Aktionen",
"ends": "Endet",
"mine": "Meine",
"starts": "Beginnt",
"status": "Status"
}
},
"cancel": "Abbrechen",
@@ -130,6 +524,9 @@
".": "Edit Invite",
"save": "Save Invite"
},
"error": {
"note": "Notiz ist ungültig."
},
"info": "Hier kannst du neue Einladungen erstellen. Um die Einladung zu bearbeiten klicke einfach auf den Bearbeiten-Button. Wenn du authoriziert bist, kannst du den persönlichen Einladungstext auch direkt unter dem Einladungslink bearbeiten oder eine Notiz hinzufügen. Danach kannst du denselben Link einfach an die einzuladene Person verschicken. Wird der Link ohne Authorizierung aufgerufen, erscheint unten auf der Seite ein Formular zur Registrierung!",
"left": "Du kannst noch {0} Einladungen erstellen.",
"noQuota": "Deine Quota für Einladungen ist leider aufgebraucht.",
@@ -260,8 +657,8 @@
"minetest": {
"accounts": {
".": "Minetest Accounts",
"create": "Minetest Account erstellen",
"confirmDelete": "Möchtest du wirklich deinen Minetest Account '{0}' löschen? Nach der Löschung kann der Account nicht wiederhergestellt werden und andere können deinen Usernamen wieder registrieren!",
"create": "Minetest Account erstellen",
"delete": "Löschen",
"deletion": "Nach der Löschung kann der Account nicht wiederhergestellt werden und andere können deinen Usernamen wieder registrieren!",
"error": {
@@ -314,7 +711,9 @@
},
"tags": {
".": "Partey Tags",
"none": "Keine"
"expires": "Läuft ab",
"none": "Keine",
"upcoming": "Bevorstehend"
},
"timeslots": {
".": "Partey Sendeplätze",
@@ -597,7 +996,8 @@
".": "Authorisieren",
"hint": "Authorisiere die Application auf Teile deines Profile zuzugreifen um dich zu authentifizieren."
},
"login": "Login"
"login": "Login",
"login.invalid": "Ungültige Anmeldedaten"
},
"status": {
".": "Status",
@@ -666,6 +1066,12 @@
"title": "Gitea"
},
"goto": "Zum Dienst",
"immich": {
"icon": "photo_prints",
"subtitle": "Photo- und Videoverwaltung",
"text": "Alternative zu Google Photos",
"title": "Immich"
},
"invite_partey": {
"icon": "cake",
"subtitle": "Einladung zur Partey",
@@ -854,6 +1260,7 @@
"error": {
"code": "Kürzel wird bereits verwendet",
"expires": "Das Ablaufdatum muss in der Zukunft liegen",
"note": "Notiz ist ungültig.",
"url": "Ungültige Url"
},
"expires": "Ablaufdatum",
@@ -873,7 +1280,7 @@
"info": "Query Parameter werden an die Ziel Url weitergereicht"
},
"save": "Speichern",
"search": "Suche",
"search": "Suchen",
"share": {
".": "Teilen",
"clipboard": {
@@ -937,6 +1344,7 @@
"username": {
".": "Username",
"error": "Bitte wähle einen anderen Usernamen aus.",
"generate": "Benutzername generieren",
"missing": "Bitte gebe einen Usernamen an."
},
"visibility": {
+420 -11
View File
@@ -5,6 +5,375 @@
".": "Advanced"
}
},
"admin": {
".": "Administration",
"actions": "Actions",
"cancel": "Cancel",
"jitsi_rooms": {
".": "Jitsi Rooms",
"confirm_delete": "Are you sure you want to delete room '{0}'?",
"create": "Create Jitsi Room",
"delete": "Delete Jitsi Room",
"edit": "Edit Jitsi Room",
"expires": "Expires",
"id": "ID",
"moderation_starts": "Moderation Starts",
"moderator": "Moderator",
"name": "Name",
"owner": "Owner",
"owner_hint": "User ID of the room owner",
"room": "Room",
"room_required": "Room is required",
"starts": "Starts",
"subject": "Subject",
"title": "Jitsi Rooms"
},
"jukebox": {
".": "Jukebox",
"activate": "Activate Jukebox",
"autoplay": "Autoplay",
"channel": "Channel",
"configuration": "Configuration",
"disable": "Disable Jukebox",
"error": {
"channel": "Channel is required",
"max_queue_size": "Must be a positive number",
"max_search_results": "Must be a positive number"
},
"max_queue_size": "Max Queue Size",
"max_search_results": "Max Search Results",
"save_config": "Save Configuration",
"status": "Status"
},
"minetest_accounts": {
".": "Minetest Accounts",
"confirm_delete": "Are you sure you want to delete minetest account '{0}'?",
"create": "Create Minetest Account",
"created": "Created",
"delete": "Delete Minetest Account",
"edit": "Edit Minetest Account",
"name": "Name",
"owner": "Owner"
},
"oidc_clients": {
".": "OIDC Clients",
"advanced_settings": "Advanced Settings",
"alias_allowed": "Alias Allowed",
"alias_quota": "Alias Quota",
"alias_subject": "Alias Subject",
"always_permitted": "Always Permitted",
"auth_methods": "Authentication Methods",
"authorize": "Require Authorization",
"backchannel_logout_session_required": "Backchannel Logout Session Required",
"backchannel_logout_uri": "Backchannel Logout URI",
"category": "Category",
"client_id": "Client ID",
"client_name": "Client Name",
"client_name_required": "Client name is required",
"client_secret": "Client Secret",
"client_id_copied": "Client ID copied to clipboard",
"confirm_delete": "Are you sure you want to delete OIDC client '{0}'?",
"confirm_new_secret": "Are you sure you want to generate a new secret for client '{0}'?",
"copy_client_id": "Copy client ID to clipboard",
"copy_secret": "Copy secret to clipboard",
"create": "Create OIDC Client",
"create_client": "Create OIDC Client",
"hide_secret": "Hide secret",
"secret_copied": "Client secret copied to clipboard",
"show_secret": "Show secret",
"delete": "Delete OIDC Client",
"edit": "Edit OIDC Client",
"edit_client": "Edit OIDC Client",
"frontchannel_logout_session_required": "Frontchannel Logout Session Required",
"frontchannel_logout_uri": "Frontchannel Logout URI",
"grant_types": "Grant Types",
"id": "ID",
"login_url": "Login URL",
"logout_settings": "Logout Settings",
"new_secret": "Generate New Secret",
"redirect_uris": "Redirect URIs",
"redirect_uris_hint": "Comma-separated list of redirect URIs",
"redirect_uris_required": "At least one redirect URI is required",
"scopes": "Scopes",
"title": "OIDC Clients",
"token_lifetime": "Token Lifetime (seconds)",
"token_lifetime_hint": "Token lifetime in seconds"
},
"partey_maps": {
".": "Partey Maps",
"confirm_delete": "Are you sure you want to delete partey map '{0}'?",
"create": "Create Partey Map",
"delete": "Delete Partey Map",
"edit": "Edit Partey Map",
"id": "ID",
"name": "Name",
"policy_type": "Policy Type",
"tags": "Tags"
},
"partey_reports": {
".": "Partey Reports",
"confirm_delete": "Are you sure you want to delete this report?",
"confirm_delete_all": "Are you sure you want to delete all reports?",
"created": "Created",
"delete": "Delete Report",
"delete_all": "Delete All",
"id": "ID",
"reported": "Reported",
"reporter": "Reporter",
"view": "View Report",
"world": "World"
},
"partey_tags": {
".": "Partey Tags",
"confirm_delete": "Are you sure you want to delete partey tag '{0}'?",
"create": "Create Partey Tag",
"delete": "Delete Partey Tag",
"edit": "Edit Partey Tag",
"expires": "Expires",
"id": "ID",
"name": "Name",
"starts": "Starts",
"target": "Target"
},
"permission_mappings": {
".": "Permission Mappings",
"addon": "Addon",
"confirm_delete": "Are you sure you want to delete permission mapping '{0}'?",
"create": "Create Permission Mapping",
"create_mapping": "Create Permission Mapping",
"delete": "Delete Permission Mapping",
"edit": "Edit Permission Mapping",
"edit_mapping": "Edit Permission Mapping",
"expires": "Expires",
"expires_question": "Expires Question",
"id": "ID",
"item": "Item",
"item_required": "Item is required",
"lifetime": "Lifetime",
"lifetime_round": "Lifetime Round",
"lifetime_unit": "Lifetime Unit",
"names": "Names",
"names_hint": "Comma-separated list of permission names",
"names_required": "At least one permission name is required",
"product": "Product",
"starts": "Starts",
"starts_question": "Starts Question",
"title": "Permission Mappings"
},
"permissions": {
".": "Permissions",
"addon": "Addon",
"confirm_delete": "Are you sure you want to delete permission '{0}'?",
"create": "Create Permission",
"delete": "Delete Permission",
"edit": "Edit Permission",
"error": {
"expires": "Invalid expiry date",
"name": "Name is required",
"starts": "Invalid start date"
},
"expires": "Expires",
"for_user": "Permissions for {0}",
"id": "ID",
"name": "Name",
"no_permissions": "No permissions found",
"search": "Search",
"search_username": "Search by Username",
"starts": "Starts"
},
"quota_mappings": {
".": "Quota Mappings",
"append": "Append",
"confirm_delete": "Are you sure you want to delete quota mapping '{0}'?",
"create": "Create Quota Mapping",
"create_mapping": "Create Quota Mapping",
"delete": "Delete Quota Mapping",
"disposable": "Disposable",
"edit": "Edit Quota Mapping",
"edit_mapping": "Edit Quota Mapping",
"id": "ID",
"items": "Items",
"items_hint": "Comma-separated list of item IDs",
"items_required": "At least one item is required",
"name": "Name",
"name_required": "Name is required",
"products": "Products",
"products_hint": "Comma-separated list of product names",
"title": "Quota Mappings",
"unit": "Unit",
"value": "Value",
"value_min": "Value must be at least 0",
"value_required": "Value is required"
},
"quotas": {
".": "Quotas",
"confirm_delete": "Are you sure you want to delete quota '{0}'?",
"create": "Create Quota",
"create_quota": "Create Quota",
"delete": "Delete Quota",
"disposable": "Disposable",
"edit": "Edit Quota",
"edit_quota": "Edit Quota",
"for_user": "Quotas for {0}",
"id": "ID",
"name": "Name",
"name_required": "Name is required",
"no_quotas": "No quotas found",
"search": "Search",
"search_username": "Search by Username",
"unit": "Unit",
"value": "Value",
"value_min": "Value must be at least 0",
"value_required": "Value is required"
},
"create": "Create",
"save": "Save",
"services": {
".": "Services",
"always_permitted": "Always Permitted",
"category": "Category",
"confirm_delete": "Are you sure you want to delete service '{0}'?",
"create": "Create Service",
"create_service": "Create Service",
"delete": "Delete Service",
"edit": "Edit Service",
"edit_service": "Edit Service",
"name": "Name",
"name_required": "Name is required",
"permission": "Permission",
"same_site": "Same Site",
"url": "URL",
"url_required": "URL is required"
},
"shortened_urls": {
".": "Shortened URLs",
"code": "Code",
"confirm_delete": "Are you sure you want to delete shortened URL '{0}'?",
"create": "Create Shortened URL",
"created": "Created",
"delete": "Delete Shortened URL",
"edit": "Edit Shortened URL",
"owner": "Owner",
"search": "Search",
"search_placeholder": "Search shortened URLs...",
"url": "URL"
},
"system_profile_fields": {
".": "System Profile Fields",
"confirm_delete": "Are you sure you want to delete profile field '{0}'?",
"create": "Create Profile Field",
"delete": "Delete Profile Field",
"edit": "Edit Profile Field",
"label": "Label",
"name": "Name",
"name_readonly": "Name cannot be changed after creation",
"name_required": "Name is required",
"required": "Required",
"title": "System Profile Fields",
"type": "Type",
"type_required": "Type is required",
"unique_value": "Unique Value",
"uniqueValue": "Unique Value"
},
"system_properties": {
".": "System Properties",
"confirm_delete": "Are you sure you want to delete property '{0}'?",
"create": "Create Property",
"delete": "Delete Property",
"edit": "Edit Property",
"key": "Key",
"key_readonly": "Key cannot be changed after creation",
"key_required": "Key is required",
"title": "System Properties",
"update_pretix": "Update Pretix Client",
"value": "Value",
"value_required": "Value is required"
},
"timeslots": {
".": "Timeslots",
"all": "All",
"apply_filter": "Apply Filter",
"confirm_delete": "Are you sure you want to delete timeslot '{0}'?",
"create": "Create Timeslot",
"delete": "Delete Timeslot",
"edit": "Edit Timeslot",
"end": "End",
"filter_owner": "Filter by Owner",
"filter_type": "Filter by Type",
"filter_visibility": "Filter by Visibility",
"id": "ID",
"owner": "Owner",
"search": "Search",
"start": "Start",
"title": "Title",
"type": "Type",
"visibility": "Visibility"
},
"title": "Admin",
"user_aliases": {
".": "User Aliases",
"alias": "Alias",
"alias_required": "Alias is required",
"confirm_delete": "Are you sure you want to delete alias '{0}'?",
"create": "Create User Alias",
"delete": "Delete User Alias",
"edit": "Edit User Alias",
"id": "ID",
"source": "Source",
"target": "Target",
"target_hint": "User ID of the target user",
"target_required": "Target user ID is required",
"title": "User Aliases",
"visibility": "Visibility",
"visibility_required": "Visibility is required"
},
"users": {
".": "Users",
"confirm_delete": "Are you sure you want to delete user '{0}'?",
"create": "Create User",
"created": "Created",
"delete": "Delete User",
"disabled": "Disabled",
"edit": "Edit User",
"error": {
"password": "Password is required",
"password2": "Password confirmation is required",
"password_mismatch": "Passwords do not match",
"status": "Status is required",
"username": "Username is required"
},
"id": "ID",
"locked": "Locked",
"password": "Password",
"password2": "Confirm Password",
"status": {
".": "Status",
"NORMAL": "Normal",
"PURGE": "Purge",
"SLEEP": "Sleep"
},
"username": "Username",
"view": "View User",
"view_permissions": "View Permissions",
"view_quotas": "View Quotas"
},
"voucher_mappings": {
".": "Voucher Mappings",
"confirm_delete": "Are you sure you want to delete voucher mapping '{0}'?",
"create": "Create Voucher Mapping",
"delete": "Delete Voucher Mapping",
"edit": "Edit Voucher Mapping",
"free": "Free",
"id": "ID",
"name": "Name",
"name_required": "Name is required",
"quota": "Quota",
"title": "Voucher Mappings",
"voucher": "Voucher",
"voucher_min": "Voucher must be at least 0",
"voucher_required": "Voucher is required"
}
},
"borrow": {
".": "Borrow",
"items": {
@@ -17,8 +386,8 @@
"MANUAL": "Manual",
"PERIOD": "Period"
},
"create": "Create new item",
"confirmDelete": "Are you sure you want to delete item '{0}'?",
"create": "Create new item",
"delete": "Delete Item",
"description": "Description",
"edit": "Edit Item",
@@ -28,9 +397,9 @@
"availability": "Please select a availability.",
"description": "Please provide a valid description.",
"email": "Please provide a valid email adress.",
"name": "Please provide a name.",
"maxDuration": "Max. Duration must be greater than min. Duration.",
"minDuration": "Min. Duration must be lower than max. Duration.",
"name": "Please provide a name.",
"slot": {
"end": "End must be after start.",
"endDay": "End day must be equal or after start day.",
@@ -49,19 +418,21 @@
"search": "Search",
"slot": {
".": "Slot",
"add": "Add slot",
"addManual": "Add manual slot",
"addPeriod": "Add period slot",
"day": {
".": "Day",
"MONDAY": "Monday",
"TUESDAY": "Tuesday",
"WEDNESDAY": "Wednesday",
"THURSDAY": "Thursday",
"FRIDAY": "Friday",
"MONDAY": "Monday",
"SATURDAY": "Saturday",
"SUNDAY": "Sunday"
"SUNDAY": "Sunday",
"THURSDAY": "Thursday",
"TUESDAY": "Tuesday",
"WEDNESDAY": "Wednesday"
},
"delete": "Remove slot",
"dublicate": "Duplicate slot",
"end": "End date",
"endDay": "End day",
"endTime": "End time",
@@ -73,13 +444,36 @@
},
"url": "Url"
},
"item": {
"name": "Item Name"
},
"proving": {
".": "Proving",
"camera": "Camera",
"flash": "Flash"
},
"request": {
"comment": "Comment",
"end": "End",
"ends": "Ends",
"error": {
"comment": "Please provide a comment.",
"end": "End must be after start.",
"start": "Start must be before end."
},
"item": "Item",
"owner": "Owner",
"start": "Start",
"started": "Started",
"user": "User"
},
"requests": {
".": "Requests"
".": "Requests",
"actions": "Actions",
"ends": "Ends",
"mine": "Mine",
"starts": "Starts",
"status": "Status"
}
},
"cancel": "Cancel",
@@ -130,6 +524,9 @@
".": "Edit Invite",
"save": "Save Invite"
},
"error": {
"note": "Note is invalid."
},
"info": "You can create new invites here. To edit an invite like adding a note or change the personal invite message just click on the edit button. If you are authorized, you can also change the texts directly on the invite page. Afterwards just send the same link to the person to invite. If not authorized, a registration formular will be shown on bottom of page.",
"left": "You have {0} invites left.",
"noQuota": "Your quota for invites is depleted.",
@@ -260,8 +657,8 @@
"minetest": {
"accounts": {
".": "Minetest Accounts",
"create": "Create Minetest Account",
"confirmDelete": "Are you sure you want to delete your Minetest Account '{0}'? Your account cannot be restored and others can claim your account name afterwards!",
"create": "Create Minetest Account",
"delete": "Delete",
"deletion": "Your account cannot be restored and others can claim your account name afterwards!",
"error": {
@@ -314,7 +711,9 @@
},
"tags": {
".": "Partey Tags",
"none": "None"
"expires": "Expires",
"none": "None",
"upcoming": "Upcoming"
},
"timeslots": {
".": "Partey Timeslots",
@@ -587,7 +986,8 @@
".": "Authorize",
"hint": "Authorize this application to access parts of your profile for authentication."
},
"login": "Login"
"login": "Login",
"login.invalid": "Invalid login credentials"
},
"status": {
".": "Status",
@@ -656,6 +1056,12 @@
"title": "Gitea"
},
"goto": "To service",
"immich": {
"icon": "photo_prints",
"subtitle": "Photo and Video Management solution",
"text": "Alternative to Google Photos",
"title": "Immich"
},
"invite_partey": {
"icon": "cake",
"subtitle": "Invite to Partey",
@@ -844,6 +1250,7 @@
"error": {
"code": "Code already in use",
"expires": "Expires must be set in future",
"note": "Note is invalid.",
"url": "Invalid url"
},
"expires": "Expires",
@@ -863,6 +1270,7 @@
"info": "Pass query parameters to target url"
},
"save": "Save",
"search": "Search",
"share": {
".": "Share",
"clipboard": {
@@ -926,6 +1334,7 @@
"username": {
".": "Username",
"error": "Please choose a different username.",
"generate": "Generate username",
"missing": "Please enter a valid username."
},
"visibility": {