add i18n management, improvements on admin handling, fix service grid
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "we-bstly-angular",
|
"name": "we-bstly-angular",
|
||||||
"version": "3.5.0",
|
"version": "3.5.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import { AdminVoucherMappingsComponent } from './pages/admin/voucher-mappings/vo
|
|||||||
import { AdminSystemProfileFieldsComponent } from './pages/admin/system-profile-fields/system-profile-fields.component';
|
import { AdminSystemProfileFieldsComponent } from './pages/admin/system-profile-fields/system-profile-fields.component';
|
||||||
import { AdminUserAliasesComponent } from './pages/admin/user-aliases/user-aliases.component';
|
import { AdminUserAliasesComponent } from './pages/admin/user-aliases/user-aliases.component';
|
||||||
import { AdminOidcClientsComponent } from './pages/admin/oidc-clients/oidc-clients.component';
|
import { AdminOidcClientsComponent } from './pages/admin/oidc-clients/oidc-clients.component';
|
||||||
|
import { AdminI18nComponent } from './pages/admin/i18n/i18n.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: 'profile/:username', component: UserComponent, canActivate: [AuthUpdateGuard] },
|
{ path: 'profile/:username', component: UserComponent, canActivate: [AuthUpdateGuard] },
|
||||||
@@ -125,7 +126,8 @@ const routes: Routes = [
|
|||||||
{ path: 'partey-maps', component: AdminParteyMapsComponent, canActivate: [AdminGuard] },
|
{ path: 'partey-maps', component: AdminParteyMapsComponent, canActivate: [AdminGuard] },
|
||||||
{ path: 'partey-tags', component: AdminParteyTagsComponent, canActivate: [AdminGuard] },
|
{ path: 'partey-tags', component: AdminParteyTagsComponent, canActivate: [AdminGuard] },
|
||||||
{ path: 'partey-reports', component: AdminParteyReportsComponent, canActivate: [AdminGuard] },
|
{ path: 'partey-reports', component: AdminParteyReportsComponent, canActivate: [AdminGuard] },
|
||||||
{ path: 'timeslots', component: AdminTimeslotsComponent, canActivate: [AdminGuard] }
|
{ path: 'timeslots', component: AdminTimeslotsComponent, canActivate: [AdminGuard] },
|
||||||
|
{ path: 'i18n', component: AdminI18nComponent, canActivate: [AdminGuard] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ path: 'unavailable', component: UnavailableComponent },
|
{ path: 'unavailable', component: UnavailableComponent },
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ import { AdminSystemProfileFieldsComponent } from './pages/admin/system-profile-
|
|||||||
import { AdminUserAliasesComponent } from './pages/admin/user-aliases/user-aliases.component';
|
import { AdminUserAliasesComponent } from './pages/admin/user-aliases/user-aliases.component';
|
||||||
import { AdminOidcClientsComponent } from './pages/admin/oidc-clients/oidc-clients.component';
|
import { AdminOidcClientsComponent } from './pages/admin/oidc-clients/oidc-clients.component';
|
||||||
import { AdminOidcClientEditDialog } from './pages/admin/oidc-clients/oidc-client.edit';
|
import { AdminOidcClientEditDialog } from './pages/admin/oidc-clients/oidc-client.edit';
|
||||||
|
import { AdminI18nComponent } from './pages/admin/i18n/i18n.component';
|
||||||
|
import { AdminI18nEditDialog } from './pages/admin/i18n/i18n.edit';
|
||||||
import { AdminVoucherMappingEditDialog } from './pages/admin/voucher-mappings/voucher-mapping.edit';
|
import { AdminVoucherMappingEditDialog } from './pages/admin/voucher-mappings/voucher-mapping.edit';
|
||||||
import { AdminSystemPropertyEditDialog } from './pages/admin/system-properties/system-property.edit';
|
import { AdminSystemPropertyEditDialog } from './pages/admin/system-properties/system-property.edit';
|
||||||
import { AdminSystemProfileFieldEditDialog } from './pages/admin/system-profile-fields/system-profile-field.edit';
|
import { AdminSystemProfileFieldEditDialog } from './pages/admin/system-profile-fields/system-profile-field.edit';
|
||||||
@@ -176,7 +178,7 @@ export class XhrInterceptor implements HttpInterceptor {
|
|||||||
AdminParteyTagsComponent, AdminParteyReportsComponent, AdminTimeslotsComponent,
|
AdminParteyTagsComponent, AdminParteyReportsComponent, AdminTimeslotsComponent,
|
||||||
AdminSystemPropertiesComponent, AdminPermissionMappingsComponent, AdminQuotaMappingsComponent,
|
AdminSystemPropertiesComponent, AdminPermissionMappingsComponent, AdminQuotaMappingsComponent,
|
||||||
AdminVoucherMappingsComponent, AdminSystemProfileFieldsComponent, AdminUserAliasesComponent,
|
AdminVoucherMappingsComponent, AdminSystemProfileFieldsComponent, AdminUserAliasesComponent,
|
||||||
AdminOidcClientsComponent
|
AdminOidcClientsComponent, AdminI18nComponent, AdminI18nEditDialog
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
|||||||
@@ -42,6 +42,10 @@
|
|||||||
[active]="rlaurls.isActive">
|
[active]="rlaurls.isActive">
|
||||||
{{'admin.shortened_urls' | i18n}}
|
{{'admin.shortened_urls' | i18n}}
|
||||||
</a>
|
</a>
|
||||||
|
<a mat-tab-link routerLink="i18n" routerLinkActive #rli18n="routerLinkActive"
|
||||||
|
[active]="rli18n.isActive">
|
||||||
|
{{'admin.i18n' | i18n}}
|
||||||
|
</a>
|
||||||
<!--
|
<!--
|
||||||
<a mat-tab-link routerLink="minetest-accounts" routerLinkActive #rlaminetest="routerLinkActive"
|
<a mat-tab-link routerLink="minetest-accounts" routerLinkActive #rlaminetest="routerLinkActive"
|
||||||
[active]="rlaminetest.isActive">
|
[active]="rlaminetest.isActive">
|
||||||
|
|||||||
@@ -3,7 +3,28 @@
|
|||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
margin: 0 8px;
|
||||||
|
flex: 0 0 150px;
|
||||||
|
|
||||||
|
&.grow {
|
||||||
|
flex: 1 1 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
@@ -33,3 +54,17 @@ mat-checkbox {
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Expired items styling */
|
||||||
|
.expired-row {
|
||||||
|
background-color: rgba(244, 67, 54, 0.1) !important;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
td {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-row:hover {
|
||||||
|
background-color: rgba(244, 67, 54, 0.15) !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<header>
|
||||||
|
<h3>{{'admin.i18n' | i18n}}</h3>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
|
||||||
|
<mat-form-field subscriptSizing="dynamic">
|
||||||
|
<mat-label>{{ 'admin.i18n.locale' | i18n }}</mat-label>
|
||||||
|
<mat-select [(ngModel)]="selectedLocale" (selectionChange)="onLocaleChange()">
|
||||||
|
@for (locale of locales; track locale) {
|
||||||
|
<mat-option [value]="locale">{{ locale }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
@if (!rawJsonMode) {
|
||||||
|
<mat-form-field class="grow" subscriptSizing="dynamic">
|
||||||
|
<mat-label>{{ 'admin.filter' | i18n }}</mat-label>
|
||||||
|
<input matInput [(ngModel)]="searchFilter" (ngModelChange)="onFilterChange()"
|
||||||
|
[placeholder]="'admin.filter_placeholder' | i18n">
|
||||||
|
@if (searchFilter) {
|
||||||
|
<button mat-icon-button matSuffix (click)="searchFilter = ''; onFilterChange()">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button mat-raised-button color="accent" (click)="createEntry()">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
{{ 'admin.i18n.create' | i18n }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button mat-raised-button [color]="rawJsonMode ? 'warn' : 'primary'" (click)="toggleRawJsonMode()">
|
||||||
|
<mat-icon>{{ rawJsonMode ? 'table_chart' : 'code' }}</mat-icon>
|
||||||
|
{{ (rawJsonMode ? 'admin.i18n.table_mode' : 'admin.i18n.raw_json_mode') | i18n }}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
@if (loading) {
|
||||||
|
<mat-progress-bar mode="indeterminate" class="loading-indicator"></mat-progress-bar>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (rawJsonMode) {
|
||||||
|
<div class="raw-json-editor">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'admin.i18n.raw_json' | i18n }}</mat-label>
|
||||||
|
<textarea matInput [(ngModel)]="rawJsonData" rows="20"
|
||||||
|
[placeholder]="'admin.i18n.raw_json_placeholder' | i18n"></textarea>
|
||||||
|
@if (rawJsonError) {
|
||||||
|
<mat-error>{{ rawJsonError }}</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
<div class="actions">
|
||||||
|
<button mat-raised-button color="primary" (click)="saveRawJson()" [disabled]="loading">
|
||||||
|
<mat-icon>save</mat-icon>
|
||||||
|
{{ 'admin.save' | i18n }}
|
||||||
|
</button>
|
||||||
|
<button mat-button (click)="toggleRawJsonMode()">
|
||||||
|
{{ 'admin.cancel' | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!rawJsonMode) {
|
||||||
|
@if (!loading && entries.length === 0 && !searchFilter) {
|
||||||
|
<div class="empty-state">
|
||||||
|
<mat-icon>translate</mat-icon>
|
||||||
|
<p>{{ 'admin.i18n.empty' | i18n }}</p>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
@if (!loading && dataSource.filteredData.length === 0 && searchFilter) {
|
||||||
|
<div class="empty-state">
|
||||||
|
<mat-icon>search_off</mat-icon>
|
||||||
|
<p>{{ 'admin.no_results' | i18n }}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table mat-table [dataSource]="dataSource" class="admin-table">
|
||||||
|
|
||||||
|
<ng-container matColumnDef="key">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>{{ 'admin.i18n.key' | i18n }}</th>
|
||||||
|
<td mat-cell *matCellDef="let entry">
|
||||||
|
<code>{{ entry.key }}</code>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="value">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>{{ 'admin.i18n.value' | i18n }}</th>
|
||||||
|
<td mat-cell *matCellDef="let entry">{{ entry.value }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>{{ 'admin.actions' | i18n }}</th>
|
||||||
|
<td mat-cell *matCellDef="let entry">
|
||||||
|
<button mat-icon-button (click)="editEntry(entry)" [matTooltip]="'admin.edit' | i18n">
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button (click)="deleteEntry(entry)" [matTooltip]="'admin.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>
|
||||||
|
|
||||||
|
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" [pageSize]="25" showFirstLastButtons
|
||||||
|
aria-label="Select page of i18n labels">
|
||||||
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
flex: 0 0 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-of-type(2) {
|
||||||
|
flex: 1 1 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.raw-json-editor {
|
||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
button + button {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { I18nManagementService } from '../../../services/admin/i18n.management.service';
|
||||||
|
import { I18nService } from '../../../services/i18n.service';
|
||||||
|
import { ConfirmDialog } from '../../../ui/confirm/confirm.component';
|
||||||
|
import { AdminI18nEditDialog } from './i18n.edit';
|
||||||
|
|
||||||
|
|
||||||
|
interface I18nEntry {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: false,
|
||||||
|
selector: 'app-admin-i18n',
|
||||||
|
templateUrl: './i18n.component.html',
|
||||||
|
styleUrls: ['../admin.scss', './i18n.component.scss']
|
||||||
|
})
|
||||||
|
export class AdminI18nComponent implements OnInit, AfterViewInit {
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||||
|
|
||||||
|
dataSource = new MatTableDataSource<I18nEntry>([]);
|
||||||
|
entries: I18nEntry[] = [];
|
||||||
|
loading: boolean = false;
|
||||||
|
selectedLocale: string = '';
|
||||||
|
locales: string[] = [];
|
||||||
|
searchFilter: string = '';
|
||||||
|
rawJsonMode: boolean = false;
|
||||||
|
rawJsonData: string = '';
|
||||||
|
rawJsonError: string = '';
|
||||||
|
|
||||||
|
displayedColumns: string[] = ['key', 'value', 'actions'];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private i18nManagementService: I18nManagementService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
public dialog: MatDialog
|
||||||
|
) {
|
||||||
|
// Set custom filter predicate to search both key and value
|
||||||
|
this.dataSource.filterPredicate = (data: I18nEntry, filter: string) => {
|
||||||
|
const searchStr = filter.toLowerCase();
|
||||||
|
return data.key.toLowerCase().includes(searchStr) ||
|
||||||
|
data.value.toLowerCase().includes(searchStr);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadLocales();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.dataSource.paginator = this.paginator;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLocales(): void {
|
||||||
|
this.i18nManagementService.getLocales().subscribe({
|
||||||
|
next: (data: string[]) => {
|
||||||
|
this.locales = data;
|
||||||
|
if (this.locales.length > 0) {
|
||||||
|
this.selectedLocale = this.i18nService.getLocale() || this.locales[0];
|
||||||
|
this.loadLabels();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading locales:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLabels(): void {
|
||||||
|
if (!this.selectedLocale) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.i18nManagementService.getLabels(this.selectedLocale).subscribe({
|
||||||
|
next: (data: any) => {
|
||||||
|
this.entries = this.flattenObject(data);
|
||||||
|
this.rawJsonData = JSON.stringify(data, null, 2);
|
||||||
|
this.rawJsonError = '';
|
||||||
|
this.dataSource.data = this.entries;
|
||||||
|
// Re-attach paginator after data change
|
||||||
|
if (this.paginator) {
|
||||||
|
this.dataSource.paginator = this.paginator;
|
||||||
|
}
|
||||||
|
this.applyFilter();
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading labels:', error);
|
||||||
|
this.loading = false;
|
||||||
|
this.entries = [];
|
||||||
|
this.dataSource.data = [];
|
||||||
|
this.rawJsonData = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
flattenObject(obj: any, parentPath: string = ''): I18nEntry[] {
|
||||||
|
const result: I18nEntry[] = [];
|
||||||
|
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
const value = obj[key];
|
||||||
|
|
||||||
|
if (key === '.') {
|
||||||
|
// Handle the special "." key - it represents the value at this level
|
||||||
|
result.push({
|
||||||
|
key: parentPath,
|
||||||
|
value: value,
|
||||||
|
path: parentPath.split('.').slice(0, -1).join('.')
|
||||||
|
});
|
||||||
|
} else if (typeof value === 'object' && value !== null) {
|
||||||
|
// Recursively flatten nested objects
|
||||||
|
const fullKey = parentPath ? `${parentPath}.${key}` : key;
|
||||||
|
result.push(...this.flattenObject(value, fullKey));
|
||||||
|
} else {
|
||||||
|
// Simple string value
|
||||||
|
const fullKey = parentPath ? `${parentPath}.${key}` : key;
|
||||||
|
result.push({
|
||||||
|
key: fullKey,
|
||||||
|
value: value,
|
||||||
|
path: parentPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
onLocaleChange(): void {
|
||||||
|
this.loadLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter(): void {
|
||||||
|
this.dataSource.filter = this.searchFilter.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterChange(): void {
|
||||||
|
this.applyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleRawJsonMode(): void {
|
||||||
|
this.rawJsonMode = !this.rawJsonMode;
|
||||||
|
if (!this.rawJsonMode && this.rawJsonError) {
|
||||||
|
// If switching back from raw mode with errors, reload
|
||||||
|
this.loadLabels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveRawJson(): void {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(this.rawJsonData);
|
||||||
|
this.rawJsonError = '';
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.i18nManagementService.setLabels(this.selectedLocale, parsed).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.loadLabels();
|
||||||
|
this.rawJsonMode = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error saving raw JSON:', error);
|
||||||
|
this.rawJsonError = error.error?.message || 'Error saving JSON data';
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
this.rawJsonError = 'Invalid JSON: ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createEntry(): void {
|
||||||
|
const dialogRef = this.dialog.open(AdminI18nEditDialog, {
|
||||||
|
data: { locale: this.selectedLocale, entry: null },
|
||||||
|
minWidth: '500px'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
this.loadLabels();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editEntry(entry: I18nEntry): void {
|
||||||
|
const dialogRef = this.dialog.open(AdminI18nEditDialog, {
|
||||||
|
data: { locale: this.selectedLocale, entry: entry },
|
||||||
|
minWidth: '500px'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
this.loadLabels();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteEntry(entry: I18nEntry): void {
|
||||||
|
const dialogRef = this.dialog.open(ConfirmDialog, {
|
||||||
|
data: {
|
||||||
|
'label': 'admin.i18n.confirm_delete',
|
||||||
|
'args': [entry.key]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
this.removeKeyFromLabels(entry.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeKeyFromLabels(key: string): void {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.i18nManagementService.getLabels(this.selectedLocale).subscribe({
|
||||||
|
next: (data: any) => {
|
||||||
|
const updated = this.deleteNestedKey(data, key);
|
||||||
|
|
||||||
|
this.i18nManagementService.setLabels(this.selectedLocale, updated).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.loadLabels();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting label:', error);
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading labels for deletion:', error);
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNestedKey(obj: any, key: string): any {
|
||||||
|
const keys = key.split('.');
|
||||||
|
const newObj = JSON.parse(JSON.stringify(obj)); // Deep clone
|
||||||
|
|
||||||
|
let current = newObj;
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
if (!current[keys[i]]) {
|
||||||
|
return newObj;
|
||||||
|
}
|
||||||
|
current = current[keys[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
delete current[keys[keys.length - 1]];
|
||||||
|
|
||||||
|
return newObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
unflattenEntries(): any {
|
||||||
|
const result: any = {};
|
||||||
|
|
||||||
|
for (const entry of this.entries) {
|
||||||
|
this.setNestedValue(result, entry.key, entry.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
setNestedValue(obj: any, path: string, value: any): void {
|
||||||
|
const keys = path.split('.');
|
||||||
|
let current = obj;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
if (!current[keys[i]]) {
|
||||||
|
current[keys[i]] = {};
|
||||||
|
}
|
||||||
|
current = current[keys[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
current[keys[keys.length - 1]] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<h2 mat-dialog-title>
|
||||||
|
{{ (data.entry ? 'admin.i18n.edit_label' : 'admin.i18n.create_label') | i18n }}
|
||||||
|
</h2>
|
||||||
|
<mat-dialog-content>
|
||||||
|
<form>
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>{{ 'admin.i18n.key' | i18n }}</mat-label>
|
||||||
|
<input matInput [(ngModel)]="key" name="key" required
|
||||||
|
[readonly]="data.entry !== null"
|
||||||
|
[placeholder]="'admin.i18n.key_placeholder' | i18n">
|
||||||
|
<mat-hint>{{ 'admin.i18n.key_hint' | i18n }}</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>{{ 'admin.i18n.value' | i18n }}</mat-label>
|
||||||
|
<textarea matInput [(ngModel)]="value" name="value" required
|
||||||
|
rows="4"
|
||||||
|
[placeholder]="'admin.i18n.value_placeholder' | i18n"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
@if (errorMessage) {
|
||||||
|
<mat-error class="error-message">{{ errorMessage }}</mat-error>
|
||||||
|
}
|
||||||
|
</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)="onSave()" [disabled]="saving || !isValid()">
|
||||||
|
@if (saving) {
|
||||||
|
<mat-spinner diameter="20"></mat-spinner>
|
||||||
|
}
|
||||||
|
{{ 'admin.save' | i18n }}
|
||||||
|
</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-dialog-content {
|
||||||
|
min-width: 500px;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { I18nManagementService } from '../../../services/admin/i18n.management.service';
|
||||||
|
|
||||||
|
interface I18nEntry {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: false,
|
||||||
|
selector: 'app-admin-i18n-edit',
|
||||||
|
templateUrl: './i18n.edit.html',
|
||||||
|
styleUrls: ['./i18n.edit.scss']
|
||||||
|
})
|
||||||
|
export class AdminI18nEditDialog {
|
||||||
|
|
||||||
|
key: string = '';
|
||||||
|
value: string = '';
|
||||||
|
saving: boolean = false;
|
||||||
|
errorMessage: string = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<AdminI18nEditDialog>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { locale: string; entry: I18nEntry | null },
|
||||||
|
private i18nManagementService: I18nManagementService
|
||||||
|
) {
|
||||||
|
if (data.entry) {
|
||||||
|
this.key = data.entry.key;
|
||||||
|
this.value = data.entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid(): boolean {
|
||||||
|
return this.key.trim().length > 0 && this.value.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel(): void {
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave(): void {
|
||||||
|
if (!this.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saving = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// Build nested object from key
|
||||||
|
const labels = this.buildNestedObject(this.key, this.value);
|
||||||
|
|
||||||
|
// Use addLabels to merge with existing labels
|
||||||
|
this.i18nManagementService.addLabels(this.data.locale, labels).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.saving = false;
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error saving label:', error);
|
||||||
|
this.errorMessage = error.error?.message || 'admin.i18n.save_error';
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buildNestedObject(key: string, value: string): any {
|
||||||
|
const keys = key.split('.');
|
||||||
|
const result: any = {};
|
||||||
|
|
||||||
|
let current = result;
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
current[keys[i]] = {};
|
||||||
|
current = current[keys[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
current[keys[keys.length - 1]] = value;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!jitsiRooms) {
|
@if (!!jitsiRooms) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (jitsiRooms.content.length > 0) {
|
||||||
<table mat-table [dataSource]="jitsiRooms.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="jitsiRooms.content" matSort (matSortChange)="updateSort($event)">
|
||||||
|
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
@@ -64,5 +65,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (jitsiRooms.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.jitsi_rooms.no_rooms' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!minetestAccounts) {
|
@if (!!minetestAccounts) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (minetestAccounts.content.length > 0) {
|
||||||
<table mat-table [dataSource]="minetestAccounts.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="minetestAccounts.content" matSort (matSortChange)="updateSort($event)">
|
||||||
|
|
||||||
<ng-container matColumnDef="name">
|
<ng-container matColumnDef="name">
|
||||||
@@ -49,5 +50,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (minetestAccounts.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.minetest_accounts.no_accounts' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!oidcClients) {
|
@if (!!oidcClients) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (oidcClients.content.length > 0) {
|
||||||
<table mat-table [dataSource]="oidcClients.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="oidcClients.content" matSort (matSortChange)="updateSort($event)">
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.oidc_clients.id' | i18n}}</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.oidc_clients.id' | i18n}}</th>
|
||||||
@@ -51,5 +52,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (oidcClients.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.oidc_clients.no_clients' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!parteyMaps) {
|
@if (!!parteyMaps) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (parteyMaps.content.length > 0) {
|
||||||
<table mat-table [dataSource]="parteyMaps.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="parteyMaps.content" matSort (matSortChange)="updateSort($event)">
|
||||||
|
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
@@ -54,5 +55,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (parteyMaps.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.partey_maps.no_maps' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!parteyReports) {
|
@if (!!parteyReports) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (parteyReports.content.length > 0) {
|
||||||
<table mat-table [dataSource]="parteyReports.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="parteyReports.content" matSort (matSortChange)="updateSort($event)">
|
||||||
|
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
@@ -59,5 +60,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (parteyReports.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.partey_reports.no_reports' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!parteyTags) {
|
@if (!!parteyTags) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (parteyTags.content.length > 0) {
|
||||||
<table mat-table [dataSource]="parteyTags.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="parteyTags.content" matSort (matSortChange)="updateSort($event)">
|
||||||
|
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
@@ -59,5 +60,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (parteyTags.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.partey_tags.no_tags' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!permissionMappings) {
|
@if (!!permissionMappings) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (permissionMappings.content.length > 0) {
|
||||||
<table mat-table [dataSource]="permissionMappings.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="permissionMappings.content" matSort (matSortChange)="updateSort($event)">
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.permission_mappings.id' | i18n}}</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.permission_mappings.id' | i18n}}</th>
|
||||||
@@ -58,5 +59,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (permissionMappings.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.permission_mappings.no_mappings' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
<mat-icon>add</mat-icon>
|
<mat-icon>add</mat-icon>
|
||||||
{{'admin.permissions.create' | i18n}}
|
{{'admin.permissions.create' | i18n}}
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-raised-button color="accent" (click)="loadAllPermissions()">
|
||||||
|
<mat-icon>list</mat-icon>
|
||||||
|
{{'admin.permissions.load_all' | i18n}}
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -62,7 +66,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [class.expired-row]="isExpired(row)"></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,23 @@ export class AdminPermissionsComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadAllPermissions(): void {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.permissionManagementService.getAllPermissionsForUser(this.selectedUsername, 'name')
|
||||||
|
.subscribe({
|
||||||
|
next: (data: any) => {
|
||||||
|
this.permissions = data;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading all permissions:', error);
|
||||||
|
this.loading = false;
|
||||||
|
this.permissions = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createPermission(): void {
|
createPermission(): void {
|
||||||
const dialogRef = this.dialog.open(AdminPermissionEditDialog, {
|
const dialogRef = this.dialog.open(AdminPermissionEditDialog, {
|
||||||
data: { username: this.selectedUsername, permission: null },
|
data: { username: this.selectedUsername, permission: null },
|
||||||
@@ -110,4 +127,11 @@ export class AdminPermissionsComponent implements OnInit {
|
|||||||
formatDate(date: string): string {
|
formatDate(date: string): string {
|
||||||
return date ? new Date(date).toLocaleString() : '-';
|
return date ? new Date(date).toLocaleString() : '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isExpired(permission: any): boolean {
|
||||||
|
if (!permission.expires) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return new Date(permission.expires) < new Date();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!quotaMappings) {
|
@if (!!quotaMappings) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (quotaMappings.content.length > 0) {
|
||||||
<table mat-table [dataSource]="quotaMappings.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="quotaMappings.content" matSort (matSortChange)="updateSort($event)">
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.quota_mappings.id' | i18n}}</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.quota_mappings.id' | i18n}}</th>
|
||||||
@@ -58,5 +59,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (quotaMappings.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.quota_mappings.no_mappings' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
<mat-icon>add</mat-icon>
|
<mat-icon>add</mat-icon>
|
||||||
{{'admin.quotas.create' | i18n}}
|
{{'admin.quotas.create' | i18n}}
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-raised-button color="accent" (click)="loadAllQuotas()" style="margin-left: 8px;">
|
||||||
|
<mat-icon>list</mat-icon>
|
||||||
|
{{'admin.quotas.load_all' | i18n}}
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -62,7 +66,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [class.expired-row]="isExpired(row)"></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,23 @@ export class AdminQuotasComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadAllQuotas(): void {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.quotaManagementService.getAllQuotasForUser(this.selectedUsername, 'name')
|
||||||
|
.subscribe({
|
||||||
|
next: (data: any) => {
|
||||||
|
this.quotas = data;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading all quotas:', error);
|
||||||
|
this.loading = false;
|
||||||
|
this.quotas = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
deleteQuota(quota: any): void {
|
deleteQuota(quota: any): void {
|
||||||
const dialogRef = this.dialog.open(ConfirmDialog, {
|
const dialogRef = this.dialog.open(ConfirmDialog, {
|
||||||
data: {
|
data: {
|
||||||
@@ -112,4 +129,8 @@ export class AdminQuotasComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isExpired(quota: any): boolean {
|
||||||
|
return quota.value <= 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!services) {
|
@if (!!services) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (services.content.length > 0) {
|
||||||
<table mat-table [dataSource]="services.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="services.content" matSort (matSortChange)="updateSort($event)">
|
||||||
|
|
||||||
<ng-container matColumnDef="name">
|
<ng-container matColumnDef="name">
|
||||||
@@ -54,5 +55,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (services.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.services.no_services' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,25 @@
|
|||||||
<header>
|
<header>
|
||||||
<h3>{{'admin.shortened_urls' | i18n}}</h3>
|
<h3>{{'admin.shortened_urls' | i18n}}</h3>
|
||||||
<span class="spacer"></span>
|
<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()">
|
<form [formGroup]="searchForm" (ngSubmit)="onSearch()">
|
||||||
<mat-form-field appearance="outline" style="width: 100%; max-width: 400px;">
|
<mat-form-field class="grow" subscriptSizing="dynamic">
|
||||||
<mat-label>{{'admin.shortened_urls.search' | i18n}}</mat-label>
|
<mat-label>{{'admin.shortened_urls.search' | i18n}}</mat-label>
|
||||||
<input matInput formControlName="search" [placeholder]="'admin.shortened_urls.search_placeholder' | i18n">
|
<input matInput [placeholder]="'admin.shortened_urls.search_placeholder' | i18n">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button mat-raised-button color="primary" type="submit" style="margin-left: 10px;">
|
<button mat-raised-button color="primary" type="submit" style="margin-left: 10px;">
|
||||||
<mat-icon>search</mat-icon>
|
<mat-icon>search</mat-icon>
|
||||||
{{'admin.shortened_urls.search' | i18n}}
|
{{'admin.shortened_urls.search' | i18n}}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</mat-card-content>
|
<button mat-raised-button color="primary" (click)="createShortenedUrl()">
|
||||||
</mat-card>
|
<mat-icon>add</mat-icon>
|
||||||
|
{{'admin.shortened_urls.create' | i18n}}
|
||||||
<br>
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
@if (!!shortenedUrls) {
|
@if (!!shortenedUrls) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (shortenedUrls.content.length > 0) {
|
||||||
<table mat-table [dataSource]="shortenedUrls.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="shortenedUrls.content" matSort (matSortChange)="updateSort($event)">
|
||||||
|
|
||||||
<ng-container matColumnDef="code">
|
<ng-container matColumnDef="code">
|
||||||
@@ -64,12 +58,13 @@
|
|||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<mat-paginator
|
<mat-paginator [pageSizeOptions]="pageSizeOptions" [length]="shortenedUrls.page.totalElements"
|
||||||
[pageSizeOptions]="pageSizeOptions"
|
[pageSize]="shortenedUrls.page.size" (page)="updatePages($event)" showFirstLastButtons>
|
||||||
[length]="shortenedUrls.page.totalElements"
|
|
||||||
[pageSize]="shortenedUrls.page.size"
|
|
||||||
(page)="updatePages($event)"
|
|
||||||
showFirstLastButtons>
|
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (shortenedUrls.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.shortened_urls.no_urls' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!systemProfileFields) {
|
@if (!!systemProfileFields) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (systemProfileFields.content.length > 0) {
|
||||||
<table mat-table [dataSource]="systemProfileFields.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="systemProfileFields.content" matSort (matSortChange)="updateSort($event)">
|
||||||
<ng-container matColumnDef="name">
|
<ng-container matColumnDef="name">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_profile_fields.name' | i18n}}</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_profile_fields.name' | i18n}}</th>
|
||||||
@@ -48,5 +49,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (systemProfileFields.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.system_profile_fields.no_fields' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
@if (!!systemProperties) {
|
@if (!!systemProperties) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (systemProperties.content.length > 0) {
|
||||||
<table mat-table [dataSource]="systemProperties.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="systemProperties.content" matSort (matSortChange)="updateSort($event)">
|
||||||
<ng-container matColumnDef="key">
|
<ng-container matColumnDef="key">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_properties.key' | i18n}}</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_properties.key' | i18n}}</th>
|
||||||
@@ -46,5 +47,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (systemProperties.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.system_properties.no_properties' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
|
|
||||||
@if (!!timeslots) {
|
@if (!!timeslots) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (timeslots.content.length > 0) {
|
||||||
<table mat-table [dataSource]="timeslots.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="timeslots.content" matSort (matSortChange)="updateSort($event)">
|
||||||
|
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
@@ -115,5 +116,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (timeslots.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.timeslots.no_timeslots' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!userAliases) {
|
@if (!!userAliases) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (userAliases.content.length > 0) {
|
||||||
<table mat-table [dataSource]="userAliases.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="userAliases.content" matSort (matSortChange)="updateSort($event)">
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.id' | i18n}}</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.id' | i18n}}</th>
|
||||||
@@ -53,5 +54,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (userAliases.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.user_aliases.no_aliases' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!users) {
|
@if (!!users) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (users.content.length > 0) {
|
||||||
<table mat-table [dataSource]="users.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="users.content" matSort (matSortChange)="updateSort($event)">
|
||||||
|
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
@@ -53,5 +54,10 @@
|
|||||||
<mat-paginator [pageSizeOptions]="pageSizeOptions" [length]="users.page.totalElements" [pageSize]="users.page.size"
|
<mat-paginator [pageSizeOptions]="pageSizeOptions" [length]="users.page.totalElements" [pageSize]="users.page.size"
|
||||||
(page)="updatePages($event)" showFirstLastButtons>
|
(page)="updatePages($event)" showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (users.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.users.no_users' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
@if (!!voucherMappings) {
|
@if (!!voucherMappings) {
|
||||||
<div>
|
<div>
|
||||||
|
@if (voucherMappings.content.length > 0) {
|
||||||
<table mat-table [dataSource]="voucherMappings.content" matSort (matSortChange)="updateSort($event)">
|
<table mat-table [dataSource]="voucherMappings.content" matSort (matSortChange)="updateSort($event)">
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.voucher_mappings.id' | i18n}}</th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.voucher_mappings.id' | i18n}}</th>
|
||||||
@@ -58,5 +59,10 @@
|
|||||||
(page)="updatePages($event)"
|
(page)="updatePages($event)"
|
||||||
showFirstLastButtons>
|
showFirstLastButtons>
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (voucherMappings.content.length === 0) {
|
||||||
|
<p style="text-align: center; margin-top: 20px;">{{'admin.voucher_mappings.no_mappings' | i18n}}</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,41 +13,29 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
@if (!baseServices) {
|
@if (!baseServices) {
|
||||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (baseServices && baseServices.length == 0) {
|
@if (baseServices && baseServices.length == 0) {
|
||||||
<p>{{'services.empty' | i18n}}</p>
|
<p>{{'services.empty' | i18n}}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (view=='grid') {
|
@if (view=='grid') {
|
||||||
<div>
|
@if (baseServices) {
|
||||||
@if (baseServices) {
|
<app-services-grid [services]="baseServices"></app-services-grid>
|
||||||
<app-services-grid [services]="baseServices"></app-services-grid>
|
}
|
||||||
}
|
@for (item of serviceCategory | keyvalue; track item) {
|
||||||
<br>
|
|
||||||
@for (item of serviceCategory | keyvalue; track item) {
|
|
||||||
<div>
|
|
||||||
<h4>{{'services.category.' + item.key | i18n}}</h4>
|
<h4>{{'services.category.' + item.key | i18n}}</h4>
|
||||||
<app-services-grid [services]="item.value"></app-services-grid>
|
<app-services-grid [services]="item.value"></app-services-grid>
|
||||||
<br>
|
}
|
||||||
</div>
|
}
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (view=='table') {
|
@if (view=='table') {
|
||||||
<div>
|
@if (baseServices) {
|
||||||
@if (baseServices) {
|
<app-services-table [services]="baseServices"></app-services-table>
|
||||||
<app-services-table [services]="baseServices"></app-services-table>
|
}
|
||||||
}
|
@for (item of serviceCategory | keyvalue; track item) {
|
||||||
<br>
|
|
||||||
@for (item of serviceCategory | keyvalue; track item) {
|
|
||||||
<div>
|
|
||||||
<h4>{{'services.category.' + item.key | i18n}}</h4>
|
<h4>{{'services.category.' + item.key | i18n}}</h4>
|
||||||
<app-services-table [services]="item.value"></app-services-table>
|
<app-services-table [services]="item.value"></app-services-table>
|
||||||
<br>
|
}
|
||||||
</div>
|
}
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class I18nManagementService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocales(): Observable<string[]> {
|
||||||
|
return this.http.get<string[]>(environment.apiUrl + "/i18n");
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabels(locale: string): Observable<any> {
|
||||||
|
return this.http.get(environment.apiUrl + "/i18n/" + locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLabels(locale: string, labels: any): Observable<void> {
|
||||||
|
return this.http.post<void>(environment.apiUrl + "/i18n/" + locale, labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
addLabels(locale: string, labels: any): Observable<void> {
|
||||||
|
return this.http.put<void>(environment.apiUrl + "/i18n/" + locale, labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteLocale(locale: string): Observable<void> {
|
||||||
|
return this.http.delete<void>(environment.apiUrl + "/i18n/" + locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
<div class="service-grid" fxLayoutGap="24px grid">
|
<div class="service-grid">
|
||||||
@for (service of services; track service) {
|
@for (service of services; track service) {
|
||||||
<div>
|
|
||||||
<mat-card>
|
<mat-card>
|
||||||
@if (service.url) {
|
@if (service.url) {
|
||||||
<a href="{{service.url}}" [target]="service.sameSite ? '_self' : '_blank'"
|
<a href="{{service.url}}" [target]="service.sameSite ? '_self' : '_blank'" color="accent">
|
||||||
color="accent">
|
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<div class="icon" mat-card-avatar>
|
<div class="icon" mat-card-avatar>
|
||||||
<mat-icon>{{'services.' + service.name + '.icon' | i18n}}</mat-icon>
|
<mat-icon>{{'services.' + service.name + '.icon' | i18n}}</mat-icon>
|
||||||
@@ -37,6 +36,5 @@
|
|||||||
<mat-card-actions>
|
<mat-card-actions>
|
||||||
</mat-card-actions>
|
</mat-card-actions>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
column-gap: 24px;
|
column-gap: 24px;
|
||||||
row-gap: 24px;
|
row-gap: 24px;
|
||||||
|
grid-auto-rows: 1fr; /* This makes all cards in each row equal height */
|
||||||
|
|
||||||
@media (min-width: 576px) {
|
@media (min-width: 576px) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
mat-card {
|
mat-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
margin: 0;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
".": "Administration",
|
".": "Administration",
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"filter": "Filtern",
|
||||||
|
"filter_placeholder": "Suchen...",
|
||||||
|
"no_results": "Keine Ergebnisse gefunden",
|
||||||
|
"showing_entries": "{0} von {1} Einträgen angezeigt",
|
||||||
"jitsi_rooms": {
|
"jitsi_rooms": {
|
||||||
".": "Jitsi-Räume",
|
".": "Jitsi-Räume",
|
||||||
"confirm_delete": "Bist du sicher, dass du den Raum '{0}' löschen möchtest?",
|
"confirm_delete": "Bist du sicher, dass du den Raum '{0}' löschen möchtest?",
|
||||||
@@ -20,6 +26,7 @@
|
|||||||
"moderation_starts": "Moderation beginnt",
|
"moderation_starts": "Moderation beginnt",
|
||||||
"moderator": "Moderator",
|
"moderator": "Moderator",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"no_rooms": "Keine Jitsi-Räume gefunden",
|
||||||
"owner": "Besitzer",
|
"owner": "Besitzer",
|
||||||
"owner_hint": "Benutzer-ID des Raumbesitzers",
|
"owner_hint": "Benutzer-ID des Raumbesitzers",
|
||||||
"room": "Raum",
|
"room": "Raum",
|
||||||
@@ -45,6 +52,27 @@
|
|||||||
"save_config": "Konfiguration speichern",
|
"save_config": "Konfiguration speichern",
|
||||||
"status": "Status"
|
"status": "Status"
|
||||||
},
|
},
|
||||||
|
"i18n": {
|
||||||
|
".": "Internationalisierung",
|
||||||
|
"title": "Internationalisierungsverwaltung",
|
||||||
|
"locale": "Sprache",
|
||||||
|
"key": "Schlüssel",
|
||||||
|
"value": "Wert",
|
||||||
|
"create": "Label erstellen",
|
||||||
|
"create_label": "I18n-Label erstellen",
|
||||||
|
"edit_label": "I18n-Label bearbeiten",
|
||||||
|
"confirm_delete": "Bist du sicher, dass du das Label '{0}' löschen möchtest?",
|
||||||
|
"key_placeholder": "z.B. admin.users.title",
|
||||||
|
"key_hint": "Verwende Punkt-Notation für verschachtelte Schlüssel",
|
||||||
|
"value_placeholder": "Übersetzungstext eingeben",
|
||||||
|
"empty": "Keine Labels für diese Sprache gefunden",
|
||||||
|
"export": "Labels exportieren",
|
||||||
|
"save_error": "Fehler beim Speichern des Labels. Bitte versuche es erneut.",
|
||||||
|
"raw_json_mode": "Raw-JSON-Modus",
|
||||||
|
"table_mode": "Tabellenmodus",
|
||||||
|
"raw_json": "Raw-JSON-Daten",
|
||||||
|
"raw_json_placeholder": "JSON-Daten hier eingeben..."
|
||||||
|
},
|
||||||
"minetest_accounts": {
|
"minetest_accounts": {
|
||||||
".": "Minetest-Accounts",
|
".": "Minetest-Accounts",
|
||||||
"confirm_delete": "Bist du sicher, dass du den Minetest-Account '{0}' löschen möchtest?",
|
"confirm_delete": "Bist du sicher, dass du den Minetest-Account '{0}' löschen möchtest?",
|
||||||
@@ -53,6 +81,7 @@
|
|||||||
"delete": "Minetest-Account löschen",
|
"delete": "Minetest-Account löschen",
|
||||||
"edit": "Minetest-Account bearbeiten",
|
"edit": "Minetest-Account bearbeiten",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"no_accounts": "Keine Minetest-Accounts gefunden",
|
||||||
"owner": "Besitzer"
|
"owner": "Besitzer"
|
||||||
},
|
},
|
||||||
"oidc_clients": {
|
"oidc_clients": {
|
||||||
@@ -79,6 +108,7 @@
|
|||||||
"create": "OIDC-Client erstellen",
|
"create": "OIDC-Client erstellen",
|
||||||
"create_client": "OIDC-Client erstellen",
|
"create_client": "OIDC-Client erstellen",
|
||||||
"hide_secret": "Secret verbergen",
|
"hide_secret": "Secret verbergen",
|
||||||
|
"no_clients": "Keine OIDC-Clients gefunden",
|
||||||
"secret_copied": "Client-Secret in Zwischenablage kopiert",
|
"secret_copied": "Client-Secret in Zwischenablage kopiert",
|
||||||
"show_secret": "Secret anzeigen",
|
"show_secret": "Secret anzeigen",
|
||||||
"delete": "OIDC-Client löschen",
|
"delete": "OIDC-Client löschen",
|
||||||
@@ -107,6 +137,7 @@
|
|||||||
"edit": "Partey-Karte bearbeiten",
|
"edit": "Partey-Karte bearbeiten",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"no_maps": "Keine Partey-Karten gefunden",
|
||||||
"policy_type": "Richtlinientyp",
|
"policy_type": "Richtlinientyp",
|
||||||
"tags": "Tags"
|
"tags": "Tags"
|
||||||
},
|
},
|
||||||
@@ -118,6 +149,7 @@
|
|||||||
"delete": "Meldung löschen",
|
"delete": "Meldung löschen",
|
||||||
"delete_all": "Alle löschen",
|
"delete_all": "Alle löschen",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"no_reports": "Keine Partey-Meldungen gefunden",
|
||||||
"reported": "Gemeldet",
|
"reported": "Gemeldet",
|
||||||
"reporter": "Melder",
|
"reporter": "Melder",
|
||||||
"view": "Meldung anzeigen",
|
"view": "Meldung anzeigen",
|
||||||
@@ -132,6 +164,7 @@
|
|||||||
"expires": "Läuft ab",
|
"expires": "Läuft ab",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"no_tags": "Keine Partey-Tags gefunden",
|
||||||
"starts": "Beginnt",
|
"starts": "Beginnt",
|
||||||
"target": "Ziel"
|
"target": "Ziel"
|
||||||
},
|
},
|
||||||
@@ -155,6 +188,7 @@
|
|||||||
"names": "Namen",
|
"names": "Namen",
|
||||||
"names_hint": "Kommagetrennte Liste von Berechtigungsnamen",
|
"names_hint": "Kommagetrennte Liste von Berechtigungsnamen",
|
||||||
"names_required": "Mindestens ein Berechtigungsname ist erforderlich",
|
"names_required": "Mindestens ein Berechtigungsname ist erforderlich",
|
||||||
|
"no_mappings": "Keine Berechtigungs-Zuordnungen gefunden",
|
||||||
"product": "Produkt",
|
"product": "Produkt",
|
||||||
"starts": "Beginnt",
|
"starts": "Beginnt",
|
||||||
"starts_question": "Beginnt-Frage",
|
"starts_question": "Beginnt-Frage",
|
||||||
@@ -175,6 +209,7 @@
|
|||||||
"expires": "Läuft ab",
|
"expires": "Läuft ab",
|
||||||
"for_user": "Berechtigungen für {0}",
|
"for_user": "Berechtigungen für {0}",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"load_all": "Alle Berechtigungen laden",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"no_permissions": "Keine Berechtigungen gefunden",
|
"no_permissions": "Keine Berechtigungen gefunden",
|
||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
@@ -197,6 +232,7 @@
|
|||||||
"items_required": "Mindestens ein Element ist erforderlich",
|
"items_required": "Mindestens ein Element ist erforderlich",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_required": "Name ist erforderlich",
|
"name_required": "Name ist erforderlich",
|
||||||
|
"no_mappings": "Keine Kontingent-Zuordnungen gefunden",
|
||||||
"products": "Produkte",
|
"products": "Produkte",
|
||||||
"products_hint": "Kommagetrennte Liste von Produktnamen",
|
"products_hint": "Kommagetrennte Liste von Produktnamen",
|
||||||
"title": "Kontingent-Zuordnungen",
|
"title": "Kontingent-Zuordnungen",
|
||||||
@@ -216,6 +252,7 @@
|
|||||||
"edit_quota": "Kontingent bearbeiten",
|
"edit_quota": "Kontingent bearbeiten",
|
||||||
"for_user": "Kontingente für {0}",
|
"for_user": "Kontingente für {0}",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"load_all": "Alle Kontingente laden",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_required": "Name ist erforderlich",
|
"name_required": "Name ist erforderlich",
|
||||||
"no_quotas": "Keine Kontingente gefunden",
|
"no_quotas": "Keine Kontingente gefunden",
|
||||||
@@ -240,6 +277,7 @@
|
|||||||
"edit_service": "Dienst bearbeiten",
|
"edit_service": "Dienst bearbeiten",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_required": "Name ist erforderlich",
|
"name_required": "Name ist erforderlich",
|
||||||
|
"no_services": "Keine Dienste gefunden",
|
||||||
"permission": "Berechtigung",
|
"permission": "Berechtigung",
|
||||||
"same_site": "Gleiche Seite",
|
"same_site": "Gleiche Seite",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
@@ -253,6 +291,7 @@
|
|||||||
"created": "Erstellt",
|
"created": "Erstellt",
|
||||||
"delete": "Kurz-URL löschen",
|
"delete": "Kurz-URL löschen",
|
||||||
"edit": "Kurz-URL bearbeiten",
|
"edit": "Kurz-URL bearbeiten",
|
||||||
|
"no_urls": "Keine kurzen URLs gefunden",
|
||||||
"owner": "Besitzer",
|
"owner": "Besitzer",
|
||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
"search_placeholder": "Kurz-URLs durchsuchen...",
|
"search_placeholder": "Kurz-URLs durchsuchen...",
|
||||||
@@ -268,6 +307,7 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_readonly": "Name kann nach der Erstellung nicht geändert werden",
|
"name_readonly": "Name kann nach der Erstellung nicht geändert werden",
|
||||||
"name_required": "Name ist erforderlich",
|
"name_required": "Name ist erforderlich",
|
||||||
|
"no_fields": "Keine Profilfelder gefunden",
|
||||||
"required": "Erforderlich",
|
"required": "Erforderlich",
|
||||||
"title": "System-Profilfelder",
|
"title": "System-Profilfelder",
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
@@ -284,6 +324,7 @@
|
|||||||
"key": "Schlüssel",
|
"key": "Schlüssel",
|
||||||
"key_readonly": "Schlüssel kann nach der Erstellung nicht geändert werden",
|
"key_readonly": "Schlüssel kann nach der Erstellung nicht geändert werden",
|
||||||
"key_required": "Schlüssel ist erforderlich",
|
"key_required": "Schlüssel ist erforderlich",
|
||||||
|
"no_properties": "Keine Eigenschaften gefunden",
|
||||||
"title": "System-Eigenschaften",
|
"title": "System-Eigenschaften",
|
||||||
"update_pretix": "Pretix-Client aktualisieren",
|
"update_pretix": "Pretix-Client aktualisieren",
|
||||||
"value": "Wert",
|
"value": "Wert",
|
||||||
@@ -302,6 +343,7 @@
|
|||||||
"filter_type": "Nach Typ filtern",
|
"filter_type": "Nach Typ filtern",
|
||||||
"filter_visibility": "Nach Sichtbarkeit filtern",
|
"filter_visibility": "Nach Sichtbarkeit filtern",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"no_timeslots": "Keine Zeitfenster gefunden",
|
||||||
"owner": "Besitzer",
|
"owner": "Besitzer",
|
||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
@@ -319,6 +361,7 @@
|
|||||||
"delete": "Benutzer-Alias löschen",
|
"delete": "Benutzer-Alias löschen",
|
||||||
"edit": "Benutzer-Alias bearbeiten",
|
"edit": "Benutzer-Alias bearbeiten",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"no_aliases": "Keine Benutzer-Aliase gefunden",
|
||||||
"source": "Quelle",
|
"source": "Quelle",
|
||||||
"target": "Ziel",
|
"target": "Ziel",
|
||||||
"target_hint": "Benutzer-ID des Zielbenutzers",
|
"target_hint": "Benutzer-ID des Zielbenutzers",
|
||||||
@@ -344,6 +387,7 @@
|
|||||||
},
|
},
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"locked": "Gesperrt",
|
"locked": "Gesperrt",
|
||||||
|
"no_users": "Keine Benutzer gefunden",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"password2": "Passwort bestätigen",
|
"password2": "Passwort bestätigen",
|
||||||
"status": {
|
"status": {
|
||||||
@@ -367,6 +411,7 @@
|
|||||||
"id": "ID",
|
"id": "ID",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_required": "Name ist erforderlich",
|
"name_required": "Name ist erforderlich",
|
||||||
|
"no_mappings": "Keine Gutschein-Zuordnungen gefunden",
|
||||||
"quota": "Kontingent",
|
"quota": "Kontingent",
|
||||||
"title": "Gutschein-Zuordnungen",
|
"title": "Gutschein-Zuordnungen",
|
||||||
"voucher": "Gutschein",
|
"voucher": "Gutschein",
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
".": "Administration",
|
".": "Administration",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"edit": "Edit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"filter": "Filter",
|
||||||
|
"filter_placeholder": "Search...",
|
||||||
|
"no_results": "No results found",
|
||||||
|
"showing_entries": "Showing {0} of {1} entries",
|
||||||
"jitsi_rooms": {
|
"jitsi_rooms": {
|
||||||
".": "Jitsi Rooms",
|
".": "Jitsi Rooms",
|
||||||
"confirm_delete": "Are you sure you want to delete room '{0}'?",
|
"confirm_delete": "Are you sure you want to delete room '{0}'?",
|
||||||
@@ -20,6 +26,7 @@
|
|||||||
"moderation_starts": "Moderation Starts",
|
"moderation_starts": "Moderation Starts",
|
||||||
"moderator": "Moderator",
|
"moderator": "Moderator",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"no_rooms": "No jitsi rooms found",
|
||||||
"owner": "Owner",
|
"owner": "Owner",
|
||||||
"owner_hint": "User ID of the room owner",
|
"owner_hint": "User ID of the room owner",
|
||||||
"room": "Room",
|
"room": "Room",
|
||||||
@@ -45,6 +52,27 @@
|
|||||||
"save_config": "Save Configuration",
|
"save_config": "Save Configuration",
|
||||||
"status": "Status"
|
"status": "Status"
|
||||||
},
|
},
|
||||||
|
"i18n": {
|
||||||
|
".": "Internationalization",
|
||||||
|
"title": "Internationalization Management",
|
||||||
|
"locale": "Locale",
|
||||||
|
"key": "Key",
|
||||||
|
"value": "Value",
|
||||||
|
"create": "Create Label",
|
||||||
|
"create_label": "Create I18n Label",
|
||||||
|
"edit_label": "Edit I18n Label",
|
||||||
|
"confirm_delete": "Are you sure you want to delete label '{0}'?",
|
||||||
|
"key_placeholder": "e.g., admin.users.title",
|
||||||
|
"key_hint": "Use dot notation for nested keys",
|
||||||
|
"value_placeholder": "Enter translation text",
|
||||||
|
"empty": "No labels found for this locale",
|
||||||
|
"export": "Export Labels",
|
||||||
|
"save_error": "Error saving label. Please try again.",
|
||||||
|
"raw_json_mode": "Raw JSON Mode",
|
||||||
|
"table_mode": "Table Mode",
|
||||||
|
"raw_json": "Raw JSON Data",
|
||||||
|
"raw_json_placeholder": "Enter JSON data here..."
|
||||||
|
},
|
||||||
"minetest_accounts": {
|
"minetest_accounts": {
|
||||||
".": "Minetest Accounts",
|
".": "Minetest Accounts",
|
||||||
"confirm_delete": "Are you sure you want to delete minetest account '{0}'?",
|
"confirm_delete": "Are you sure you want to delete minetest account '{0}'?",
|
||||||
@@ -53,6 +81,7 @@
|
|||||||
"delete": "Delete Minetest Account",
|
"delete": "Delete Minetest Account",
|
||||||
"edit": "Edit Minetest Account",
|
"edit": "Edit Minetest Account",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"no_accounts": "No minetest accounts found",
|
||||||
"owner": "Owner"
|
"owner": "Owner"
|
||||||
},
|
},
|
||||||
"oidc_clients": {
|
"oidc_clients": {
|
||||||
@@ -91,6 +120,7 @@
|
|||||||
"login_url": "Login URL",
|
"login_url": "Login URL",
|
||||||
"logout_settings": "Logout Settings",
|
"logout_settings": "Logout Settings",
|
||||||
"new_secret": "Generate New Secret",
|
"new_secret": "Generate New Secret",
|
||||||
|
"no_clients": "No OIDC clients found",
|
||||||
"redirect_uris": "Redirect URIs",
|
"redirect_uris": "Redirect URIs",
|
||||||
"redirect_uris_hint": "Comma-separated list of redirect URIs",
|
"redirect_uris_hint": "Comma-separated list of redirect URIs",
|
||||||
"redirect_uris_required": "At least one redirect URI is required",
|
"redirect_uris_required": "At least one redirect URI is required",
|
||||||
@@ -107,6 +137,7 @@
|
|||||||
"edit": "Edit Partey Map",
|
"edit": "Edit Partey Map",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"no_maps": "No partey maps found",
|
||||||
"policy_type": "Policy Type",
|
"policy_type": "Policy Type",
|
||||||
"tags": "Tags"
|
"tags": "Tags"
|
||||||
},
|
},
|
||||||
@@ -118,6 +149,7 @@
|
|||||||
"delete": "Delete Report",
|
"delete": "Delete Report",
|
||||||
"delete_all": "Delete All",
|
"delete_all": "Delete All",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"no_reports": "No partey reports found",
|
||||||
"reported": "Reported",
|
"reported": "Reported",
|
||||||
"reporter": "Reporter",
|
"reporter": "Reporter",
|
||||||
"view": "View Report",
|
"view": "View Report",
|
||||||
@@ -132,6 +164,7 @@
|
|||||||
"expires": "Expires",
|
"expires": "Expires",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"no_tags": "No partey tags found",
|
||||||
"starts": "Starts",
|
"starts": "Starts",
|
||||||
"target": "Target"
|
"target": "Target"
|
||||||
},
|
},
|
||||||
@@ -155,6 +188,7 @@
|
|||||||
"names": "Names",
|
"names": "Names",
|
||||||
"names_hint": "Comma-separated list of permission names",
|
"names_hint": "Comma-separated list of permission names",
|
||||||
"names_required": "At least one permission name is required",
|
"names_required": "At least one permission name is required",
|
||||||
|
"no_mappings": "No permission mappings found",
|
||||||
"product": "Product",
|
"product": "Product",
|
||||||
"starts": "Starts",
|
"starts": "Starts",
|
||||||
"starts_question": "Starts Question",
|
"starts_question": "Starts Question",
|
||||||
@@ -175,6 +209,7 @@
|
|||||||
"expires": "Expires",
|
"expires": "Expires",
|
||||||
"for_user": "Permissions for {0}",
|
"for_user": "Permissions for {0}",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"load_all": "Load All Permissions",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"no_permissions": "No permissions found",
|
"no_permissions": "No permissions found",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
@@ -197,6 +232,7 @@
|
|||||||
"items_required": "At least one item is required",
|
"items_required": "At least one item is required",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_required": "Name is required",
|
"name_required": "Name is required",
|
||||||
|
"no_mappings": "No quota mappings found",
|
||||||
"products": "Products",
|
"products": "Products",
|
||||||
"products_hint": "Comma-separated list of product names",
|
"products_hint": "Comma-separated list of product names",
|
||||||
"title": "Quota Mappings",
|
"title": "Quota Mappings",
|
||||||
@@ -216,6 +252,7 @@
|
|||||||
"edit_quota": "Edit Quota",
|
"edit_quota": "Edit Quota",
|
||||||
"for_user": "Quotas for {0}",
|
"for_user": "Quotas for {0}",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"load_all": "Load All Quotas",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_required": "Name is required",
|
"name_required": "Name is required",
|
||||||
"no_quotas": "No quotas found",
|
"no_quotas": "No quotas found",
|
||||||
@@ -240,6 +277,7 @@
|
|||||||
"edit_service": "Edit Service",
|
"edit_service": "Edit Service",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_required": "Name is required",
|
"name_required": "Name is required",
|
||||||
|
"no_services": "No services found",
|
||||||
"permission": "Permission",
|
"permission": "Permission",
|
||||||
"same_site": "Same Site",
|
"same_site": "Same Site",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
@@ -253,6 +291,7 @@
|
|||||||
"created": "Created",
|
"created": "Created",
|
||||||
"delete": "Delete Shortened URL",
|
"delete": "Delete Shortened URL",
|
||||||
"edit": "Edit Shortened URL",
|
"edit": "Edit Shortened URL",
|
||||||
|
"no_urls": "No shortened URLs found",
|
||||||
"owner": "Owner",
|
"owner": "Owner",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"search_placeholder": "Search shortened URLs...",
|
"search_placeholder": "Search shortened URLs...",
|
||||||
@@ -268,6 +307,7 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_readonly": "Name cannot be changed after creation",
|
"name_readonly": "Name cannot be changed after creation",
|
||||||
"name_required": "Name is required",
|
"name_required": "Name is required",
|
||||||
|
"no_fields": "No profile fields found",
|
||||||
"required": "Required",
|
"required": "Required",
|
||||||
"title": "System Profile Fields",
|
"title": "System Profile Fields",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
@@ -284,6 +324,7 @@
|
|||||||
"key": "Key",
|
"key": "Key",
|
||||||
"key_readonly": "Key cannot be changed after creation",
|
"key_readonly": "Key cannot be changed after creation",
|
||||||
"key_required": "Key is required",
|
"key_required": "Key is required",
|
||||||
|
"no_properties": "No properties found",
|
||||||
"title": "System Properties",
|
"title": "System Properties",
|
||||||
"update_pretix": "Update Pretix Client",
|
"update_pretix": "Update Pretix Client",
|
||||||
"value": "Value",
|
"value": "Value",
|
||||||
@@ -302,6 +343,7 @@
|
|||||||
"filter_type": "Filter by Type",
|
"filter_type": "Filter by Type",
|
||||||
"filter_visibility": "Filter by Visibility",
|
"filter_visibility": "Filter by Visibility",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"no_timeslots": "No timeslots found",
|
||||||
"owner": "Owner",
|
"owner": "Owner",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
@@ -319,6 +361,7 @@
|
|||||||
"delete": "Delete User Alias",
|
"delete": "Delete User Alias",
|
||||||
"edit": "Edit User Alias",
|
"edit": "Edit User Alias",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"no_aliases": "No user aliases found",
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
"target": "Target",
|
"target": "Target",
|
||||||
"target_hint": "User ID of the target user",
|
"target_hint": "User ID of the target user",
|
||||||
@@ -344,6 +387,7 @@
|
|||||||
},
|
},
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"locked": "Locked",
|
"locked": "Locked",
|
||||||
|
"no_users": "No users found",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"password2": "Confirm Password",
|
"password2": "Confirm Password",
|
||||||
"status": {
|
"status": {
|
||||||
@@ -367,6 +411,7 @@
|
|||||||
"id": "ID",
|
"id": "ID",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_required": "Name is required",
|
"name_required": "Name is required",
|
||||||
|
"no_mappings": "No voucher mappings found",
|
||||||
"quota": "Quota",
|
"quota": "Quota",
|
||||||
"title": "Voucher Mappings",
|
"title": "Voucher Mappings",
|
||||||
"voucher": "Voucher",
|
"voucher": "Voucher",
|
||||||
|
|||||||
Reference in New Issue
Block a user