add i18n management, improvements on admin handling, fix service grid

This commit is contained in:
_Bastler
2025-11-09 16:27:28 +01:00
parent c0b9aac823
commit 4afa2c8b9a
38 changed files with 1543 additions and 683 deletions
+1 -1
View File
@@ -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",
+3 -1
View File
@@ -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 },
+3 -1
View File
@@ -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,
+4
View File
@@ -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">
+35
View File
@@ -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 {
@@ -32,4 +53,18 @@ mat-form-field {
mat-checkbox { 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;
}
}
}
+284
View File
@@ -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;
}
}
+34
View File
@@ -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>
+18
View File
@@ -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;
}
+82
View File
@@ -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,60 +9,66 @@
@if (!!jitsiRooms) { @if (!!jitsiRooms) {
<div> <div>
<table mat-table [dataSource]="jitsiRooms.content" matSort (matSortChange)="updateSort($event)"> @if (jitsiRooms.content.length > 0) {
<table mat-table [dataSource]="jitsiRooms.content" matSort (matSortChange)="updateSort($event)">
<ng-container matColumnDef="id"> <ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.id' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.id' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.id}} </td> <td mat-cell *matCellDef="let room"> {{room.id}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="owner"> <ng-container matColumnDef="owner">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.owner' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.owner' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.owner}} </td> <td mat-cell *matCellDef="let room"> {{room.owner}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="room"> <ng-container matColumnDef="room">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.room' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.room' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.room}} </td> <td mat-cell *matCellDef="let room"> {{room.room}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="starts"> <ng-container matColumnDef="starts">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.starts' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.starts' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.starts | date:'short'}} </td> <td mat-cell *matCellDef="let room"> {{room.starts | date:'short'}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="moderationStarts"> <ng-container matColumnDef="moderationStarts">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.moderation_starts' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.moderation_starts' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.moderationStarts | date:'short'}} </td> <td mat-cell *matCellDef="let room"> {{room.moderationStarts | date:'short'}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="expires"> <ng-container matColumnDef="expires">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.expires' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.jitsi_rooms.expires' | i18n}} </th>
<td mat-cell *matCellDef="let room"> {{room.expires | date:'short'}} </td> <td mat-cell *matCellDef="let room"> {{room.expires | date:'short'}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let room"> <td mat-cell *matCellDef="let room">
<button mat-icon-button (click)="editRoom(room)" [title]="'admin.jitsi_rooms.edit' | i18n"> <button mat-icon-button (click)="editRoom(room)" [title]="'admin.jitsi_rooms.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteJitsiRoom(room.id)" [title]="'admin.jitsi_rooms.delete' | i18n"> <button mat-icon-button (click)="deleteJitsiRoom(room.id)" [title]="'admin.jitsi_rooms.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="jitsiRooms.page.totalElements" [length]="jitsiRooms.page.totalElements"
[pageSize]="jitsiRooms.page.size" [pageSize]="jitsiRooms.page.size"
(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,45 +9,51 @@
@if (!!minetestAccounts) { @if (!!minetestAccounts) {
<div> <div>
<table mat-table [dataSource]="minetestAccounts.content" matSort (matSortChange)="updateSort($event)"> @if (minetestAccounts.content.length > 0) {
<table mat-table [dataSource]="minetestAccounts.content" matSort (matSortChange)="updateSort($event)">
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.minetest_accounts.name' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.minetest_accounts.name' | i18n}} </th>
<td mat-cell *matCellDef="let account"> {{account.name}} </td> <td mat-cell *matCellDef="let account"> {{account.name}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="owner"> <ng-container matColumnDef="owner">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.minetest_accounts.owner' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.minetest_accounts.owner' | i18n}} </th>
<td mat-cell *matCellDef="let account"> {{account.owner}} </td> <td mat-cell *matCellDef="let account"> {{account.owner}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="created"> <ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.minetest_accounts.created' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.minetest_accounts.created' | i18n}} </th>
<td mat-cell *matCellDef="let account"> {{account.created | date:'short'}} </td> <td mat-cell *matCellDef="let account"> {{account.created | date:'short'}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let account"> <td mat-cell *matCellDef="let account">
<button mat-icon-button [title]="'admin.minetest_accounts.edit' | i18n"> <button mat-icon-button [title]="'admin.minetest_accounts.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteMinetestAccount(account.name)" [title]="'admin.minetest_accounts.delete' | i18n"> <button mat-icon-button (click)="deleteMinetestAccount(account.name)" [title]="'admin.minetest_accounts.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="minetestAccounts.page.totalElements" [length]="minetestAccounts.page.totalElements"
[pageSize]="minetestAccounts.page.size" [pageSize]="minetestAccounts.page.size"
(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,47 +9,53 @@
@if (!!oidcClients) { @if (!!oidcClients) {
<div> <div>
<table mat-table [dataSource]="oidcClients.content" matSort (matSortChange)="updateSort($event)"> @if (oidcClients.content.length > 0) {
<ng-container matColumnDef="id"> <table mat-table [dataSource]="oidcClients.content" matSort (matSortChange)="updateSort($event)">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.oidc_clients.id' | i18n}}</th> <ng-container matColumnDef="id">
<td mat-cell *matCellDef="let client">{{client.id}}</td> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.oidc_clients.id' | i18n}}</th>
</ng-container> <td mat-cell *matCellDef="let client">{{client.id}}</td>
</ng-container>
<ng-container matColumnDef="clientId"> <ng-container matColumnDef="clientId">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.oidc_clients.client_id' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.oidc_clients.client_id' | i18n}}</th>
<td mat-cell *matCellDef="let client">{{client.clientId}}</td> <td mat-cell *matCellDef="let client">{{client.clientId}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="clientName"> <ng-container matColumnDef="clientName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.oidc_clients.client_name' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.oidc_clients.client_name' | i18n}}</th>
<td mat-cell *matCellDef="let client">{{client.clientName}}</td> <td mat-cell *matCellDef="let client">{{client.clientName}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let client"> <td mat-cell *matCellDef="let client">
<button mat-icon-button (click)="editClient(client)" [title]="'admin.oidc_clients.edit' | i18n"> <button mat-icon-button (click)="editClient(client)" [title]="'admin.oidc_clients.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="createNewSecret(client)" [title]="'admin.oidc_clients.new_secret' | i18n"> <button mat-icon-button (click)="createNewSecret(client)" [title]="'admin.oidc_clients.new_secret' | i18n">
<mat-icon>vpn_key</mat-icon> <mat-icon>vpn_key</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteClient(client)" [title]="'admin.oidc_clients.delete' | i18n"> <button mat-icon-button (click)="deleteClient(client)" [title]="'admin.oidc_clients.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="oidcClients.page.totalElements" [length]="oidcClients.page.totalElements"
[pageSize]="oidcClients.page.size" [pageSize]="oidcClients.page.size"
(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,50 +9,56 @@
@if (!!parteyMaps) { @if (!!parteyMaps) {
<div> <div>
<table mat-table [dataSource]="parteyMaps.content" matSort (matSortChange)="updateSort($event)"> @if (parteyMaps.content.length > 0) {
<table mat-table [dataSource]="parteyMaps.content" matSort (matSortChange)="updateSort($event)">
<ng-container matColumnDef="id"> <ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.partey_maps.id' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.partey_maps.id' | i18n}} </th>
<td mat-cell *matCellDef="let map"> {{map.id}} </td> <td mat-cell *matCellDef="let map"> {{map.id}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'admin.partey_maps.name' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'admin.partey_maps.name' | i18n}} </th>
<td mat-cell *matCellDef="let map"> {{map.name}} </td> <td mat-cell *matCellDef="let map"> {{map.name}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="policyType"> <ng-container matColumnDef="policyType">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_maps.policy_type' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.partey_maps.policy_type' | i18n}} </th>
<td mat-cell *matCellDef="let map"> {{map.policyType}} </td> <td mat-cell *matCellDef="let map"> {{map.policyType}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="tags"> <ng-container matColumnDef="tags">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_maps.tags' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.partey_maps.tags' | i18n}} </th>
<td mat-cell *matCellDef="let map"> {{map.tags ? map.tags.join(', ') : '-'}} </td> <td mat-cell *matCellDef="let map"> {{map.tags ? map.tags.join(', ') : '-'}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let map"> <td mat-cell *matCellDef="let map">
<button mat-icon-button [title]="'admin.partey_maps.edit' | i18n"> <button mat-icon-button [title]="'admin.partey_maps.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteParteyMap(map.id)" [title]="'admin.partey_maps.delete' | i18n"> <button mat-icon-button (click)="deleteParteyMap(map.id)" [title]="'admin.partey_maps.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="parteyMaps.page.totalElements" [length]="parteyMaps.page.totalElements"
[pageSize]="parteyMaps.page.size" [pageSize]="parteyMaps.page.size"
(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,55 +9,61 @@
@if (!!parteyReports) { @if (!!parteyReports) {
<div> <div>
<table mat-table [dataSource]="parteyReports.content" matSort (matSortChange)="updateSort($event)"> @if (parteyReports.content.length > 0) {
<table mat-table [dataSource]="parteyReports.content" matSort (matSortChange)="updateSort($event)">
<ng-container matColumnDef="id"> <ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.partey_reports.id' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.partey_reports.id' | i18n}} </th>
<td mat-cell *matCellDef="let report"> {{report.id}} </td> <td mat-cell *matCellDef="let report"> {{report.id}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="reporterUserUuid"> <ng-container matColumnDef="reporterUserUuid">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_reports.reporter' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.partey_reports.reporter' | i18n}} </th>
<td mat-cell *matCellDef="let report"> {{report.reporterUserUuid}} </td> <td mat-cell *matCellDef="let report"> {{report.reporterUserUuid}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="reportedUserUuid"> <ng-container matColumnDef="reportedUserUuid">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_reports.reported' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.partey_reports.reported' | i18n}} </th>
<td mat-cell *matCellDef="let report"> {{report.reportedUserUuid}} </td> <td mat-cell *matCellDef="let report"> {{report.reportedUserUuid}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="reportWorldSlug"> <ng-container matColumnDef="reportWorldSlug">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_reports.world' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.partey_reports.world' | i18n}} </th>
<td mat-cell *matCellDef="let report"> {{report.reportWorldSlug}} </td> <td mat-cell *matCellDef="let report"> {{report.reportWorldSlug}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="created"> <ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef mat-sort-header="created"> {{'admin.partey_reports.created' | i18n}} </th> <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> <td mat-cell *matCellDef="let report"> {{report.created | date:'short'}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let report"> <td mat-cell *matCellDef="let report">
<button mat-icon-button [title]="'admin.partey_reports.view' | i18n"> <button mat-icon-button [title]="'admin.partey_reports.view' | i18n">
<mat-icon>visibility</mat-icon> <mat-icon>visibility</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteParteyReport(report.id)" [title]="'admin.partey_reports.delete' | i18n"> <button mat-icon-button (click)="deleteParteyReport(report.id)" [title]="'admin.partey_reports.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="parteyReports.page.totalElements" [length]="parteyReports.page.totalElements"
[pageSize]="parteyReports.page.size" [pageSize]="parteyReports.page.size"
(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,55 +9,61 @@
@if (!!parteyTags) { @if (!!parteyTags) {
<div> <div>
<table mat-table [dataSource]="parteyTags.content" matSort (matSortChange)="updateSort($event)"> @if (parteyTags.content.length > 0) {
<table mat-table [dataSource]="parteyTags.content" matSort (matSortChange)="updateSort($event)">
<ng-container matColumnDef="id"> <ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.partey_tags.id' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.partey_tags.id' | i18n}} </th>
<td mat-cell *matCellDef="let tag"> {{tag.id}} </td> <td mat-cell *matCellDef="let tag"> {{tag.id}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="target"> <ng-container matColumnDef="target">
<th mat-header-cell *matHeaderCellDef mat-sort-header="target"> {{'admin.partey_tags.target' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="target"> {{'admin.partey_tags.target' | i18n}} </th>
<td mat-cell *matCellDef="let tag"> {{tag.target}} </td> <td mat-cell *matCellDef="let tag"> {{tag.target}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'admin.partey_tags.name' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'admin.partey_tags.name' | i18n}} </th>
<td mat-cell *matCellDef="let tag"> {{tag.name}} </td> <td mat-cell *matCellDef="let tag"> {{tag.name}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="starts"> <ng-container matColumnDef="starts">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_tags.starts' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.partey_tags.starts' | i18n}} </th>
<td mat-cell *matCellDef="let tag"> {{formatDate(tag.starts)}} </td> <td mat-cell *matCellDef="let tag"> {{formatDate(tag.starts)}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="expires"> <ng-container matColumnDef="expires">
<th mat-header-cell *matHeaderCellDef> {{'admin.partey_tags.expires' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.partey_tags.expires' | i18n}} </th>
<td mat-cell *matCellDef="let tag"> {{formatDate(tag.expires)}} </td> <td mat-cell *matCellDef="let tag"> {{formatDate(tag.expires)}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let tag"> <td mat-cell *matCellDef="let tag">
<button mat-icon-button [title]="'admin.partey_tags.edit' | i18n"> <button mat-icon-button [title]="'admin.partey_tags.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteParteyTag(tag.id)" [title]="'admin.partey_tags.delete' | i18n"> <button mat-icon-button (click)="deleteParteyTag(tag.id)" [title]="'admin.partey_tags.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="parteyTags.page.totalElements" [length]="parteyTags.page.totalElements"
[pageSize]="parteyTags.page.size" [pageSize]="parteyTags.page.size"
(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,54 +9,60 @@
@if (!!permissionMappings) { @if (!!permissionMappings) {
<div> <div>
<table mat-table [dataSource]="permissionMappings.content" matSort (matSortChange)="updateSort($event)"> @if (permissionMappings.content.length > 0) {
<ng-container matColumnDef="id"> <table mat-table [dataSource]="permissionMappings.content" matSort (matSortChange)="updateSort($event)">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.permission_mappings.id' | i18n}}</th> <ng-container matColumnDef="id">
<td mat-cell *matCellDef="let mapping">{{mapping.id}}</td> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.permission_mappings.id' | i18n}}</th>
</ng-container> <td mat-cell *matCellDef="let mapping">{{mapping.id}}</td>
</ng-container>
<ng-container matColumnDef="item"> <ng-container matColumnDef="item">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.permission_mappings.item' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.permission_mappings.item' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.item}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.item}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="names"> <ng-container matColumnDef="names">
<th mat-header-cell *matHeaderCellDef>{{'admin.permission_mappings.names' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.permission_mappings.names' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.names?.join(', ')}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.names?.join(', ')}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="lifetime"> <ng-container matColumnDef="lifetime">
<th mat-header-cell *matHeaderCellDef>{{'admin.permission_mappings.lifetime' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.permission_mappings.lifetime' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.lifetime}} {{mapping.lifetimeUnit}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.lifetime}} {{mapping.lifetimeUnit}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="product"> <ng-container matColumnDef="product">
<th mat-header-cell *matHeaderCellDef>{{'admin.permission_mappings.product' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.permission_mappings.product' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.product}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.product}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let mapping"> <td mat-cell *matCellDef="let mapping">
<button mat-icon-button (click)="editMapping(mapping)" [title]="'admin.permission_mappings.edit' | i18n"> <button mat-icon-button (click)="editMapping(mapping)" [title]="'admin.permission_mappings.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteMapping(mapping)" [title]="'admin.permission_mappings.delete' | i18n"> <button mat-icon-button (click)="deleteMapping(mapping)" [title]="'admin.permission_mappings.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="permissionMappings.page.totalElements" [length]="permissionMappings.page.totalElements"
[pageSize]="permissionMappings.page.size" [pageSize]="permissionMappings.page.size"
(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,54 +9,60 @@
@if (!!quotaMappings) { @if (!!quotaMappings) {
<div> <div>
<table mat-table [dataSource]="quotaMappings.content" matSort (matSortChange)="updateSort($event)"> @if (quotaMappings.content.length > 0) {
<ng-container matColumnDef="id"> <table mat-table [dataSource]="quotaMappings.content" matSort (matSortChange)="updateSort($event)">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.quota_mappings.id' | i18n}}</th> <ng-container matColumnDef="id">
<td mat-cell *matCellDef="let mapping">{{mapping.id}}</td> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.quota_mappings.id' | i18n}}</th>
</ng-container> <td mat-cell *matCellDef="let mapping">{{mapping.id}}</td>
</ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.quota_mappings.name' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.quota_mappings.name' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.name}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.name}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="value"> <ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.quota_mappings.value' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.quota_mappings.value' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.value}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.value}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="unit"> <ng-container matColumnDef="unit">
<th mat-header-cell *matHeaderCellDef>{{'admin.quota_mappings.unit' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.quota_mappings.unit' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.unit}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.unit}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="items"> <ng-container matColumnDef="items">
<th mat-header-cell *matHeaderCellDef>{{'admin.quota_mappings.items' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.quota_mappings.items' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.items?.join(', ')}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.items?.join(', ')}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let mapping"> <td mat-cell *matCellDef="let mapping">
<button mat-icon-button (click)="editMapping(mapping)" [title]="'admin.quota_mappings.edit' | i18n"> <button mat-icon-button (click)="editMapping(mapping)" [title]="'admin.quota_mappings.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteMapping(mapping)" [title]="'admin.quota_mappings.delete' | i18n"> <button mat-icon-button (click)="deleteMapping(mapping)" [title]="'admin.quota_mappings.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="quotaMappings.page.totalElements" [length]="quotaMappings.page.totalElements"
[pageSize]="quotaMappings.page.size" [pageSize]="quotaMappings.page.size"
(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,50 +9,56 @@
@if (!!services) { @if (!!services) {
<div> <div>
<table mat-table [dataSource]="services.content" matSort (matSortChange)="updateSort($event)"> @if (services.content.length > 0) {
<table mat-table [dataSource]="services.content" matSort (matSortChange)="updateSort($event)">
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.services.name' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.services.name' | i18n}} </th>
<td mat-cell *matCellDef="let service"> {{service.name}} </td> <td mat-cell *matCellDef="let service"> {{service.name}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="url"> <ng-container matColumnDef="url">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.services.url' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.services.url' | i18n}} </th>
<td mat-cell *matCellDef="let service"> {{service.url}} </td> <td mat-cell *matCellDef="let service"> {{service.url}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="category"> <ng-container matColumnDef="category">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.services.category' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.services.category' | i18n}} </th>
<td mat-cell *matCellDef="let service"> {{service.category}} </td> <td mat-cell *matCellDef="let service"> {{service.category}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="permission"> <ng-container matColumnDef="permission">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.services.permission' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.services.permission' | i18n}} </th>
<td mat-cell *matCellDef="let service"> {{service.permission}} </td> <td mat-cell *matCellDef="let service"> {{service.permission}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let service"> <td mat-cell *matCellDef="let service">
<button mat-icon-button (click)="editService(service)" [title]="'admin.services.edit' | i18n"> <button mat-icon-button (click)="editService(service)" [title]="'admin.services.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteService(service)" [title]="'admin.services.delete' | i18n"> <button mat-icon-button (click)="deleteService(service)" [title]="'admin.services.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="services.page.totalElements" [length]="services.page.totalElements"
[pageSize]="services.page.size" [pageSize]="services.page.size"
(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>
<form [formGroup]="searchForm" (ngSubmit)="onSearch()">
<mat-form-field class="grow" subscriptSizing="dynamic">
<mat-label>{{'admin.shortened_urls.search' | i18n}}</mat-label>
<input matInput [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>
<button mat-raised-button color="primary" (click)="createShortenedUrl()"> <button mat-raised-button color="primary" (click)="createShortenedUrl()">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
{{'admin.shortened_urls.create' | i18n}} {{'admin.shortened_urls.create' | i18n}}
</button> </button>
</header> </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>
@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,44 +9,50 @@
@if (!!systemProfileFields) { @if (!!systemProfileFields) {
<div> <div>
<table mat-table [dataSource]="systemProfileFields.content" matSort (matSortChange)="updateSort($event)"> @if (systemProfileFields.content.length > 0) {
<ng-container matColumnDef="name"> <table mat-table [dataSource]="systemProfileFields.content" matSort (matSortChange)="updateSort($event)">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_profile_fields.name' | i18n}}</th> <ng-container matColumnDef="name">
<td mat-cell *matCellDef="let field">{{field.name}}</td> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_profile_fields.name' | i18n}}</th>
</ng-container> <td mat-cell *matCellDef="let field">{{field.name}}</td>
</ng-container>
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_profile_fields.type' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_profile_fields.type' | i18n}}</th>
<td mat-cell *matCellDef="let field">{{field.type}}</td> <td mat-cell *matCellDef="let field">{{field.type}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="uniqueValue"> <ng-container matColumnDef="uniqueValue">
<th mat-header-cell *matHeaderCellDef>{{'admin.system_profile_fields.uniqueValue' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.system_profile_fields.uniqueValue' | i18n}}</th>
<td mat-cell *matCellDef="let field">{{field.uniqueValue ? 'Yes' : 'No'}}</td> <td mat-cell *matCellDef="let field">{{field.uniqueValue ? 'Yes' : 'No'}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let field"> <td mat-cell *matCellDef="let field">
<button mat-icon-button (click)="editField(field)" [title]="'admin.system_profile_fields.edit' | i18n"> <button mat-icon-button (click)="editField(field)" [title]="'admin.system_profile_fields.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteField(field)" [title]="'admin.system_profile_fields.delete' | i18n"> <button mat-icon-button (click)="deleteField(field)" [title]="'admin.system_profile_fields.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="systemProfileFields.page.totalElements" [length]="systemProfileFields.page.totalElements"
[pageSize]="systemProfileFields.page.size" [pageSize]="systemProfileFields.page.size"
(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,39 +12,45 @@
@if (!!systemProperties) { @if (!!systemProperties) {
<div> <div>
<table mat-table [dataSource]="systemProperties.content" matSort (matSortChange)="updateSort($event)"> @if (systemProperties.content.length > 0) {
<ng-container matColumnDef="key"> <table mat-table [dataSource]="systemProperties.content" matSort (matSortChange)="updateSort($event)">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_properties.key' | i18n}}</th> <ng-container matColumnDef="key">
<td mat-cell *matCellDef="let property">{{property.key}}</td> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_properties.key' | i18n}}</th>
</ng-container> <td mat-cell *matCellDef="let property">{{property.key}}</td>
</ng-container>
<ng-container matColumnDef="value"> <ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_properties.value' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.system_properties.value' | i18n}}</th>
<td mat-cell *matCellDef="let property">{{property.value}}</td> <td mat-cell *matCellDef="let property">{{property.value}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let property"> <td mat-cell *matCellDef="let property">
<button mat-icon-button (click)="editProperty(property)" [title]="'admin.system_properties.edit' | i18n"> <button mat-icon-button (click)="editProperty(property)" [title]="'admin.system_properties.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteProperty(property)" [title]="'admin.system_properties.delete' | i18n"> <button mat-icon-button (click)="deleteProperty(property)" [title]="'admin.system_properties.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="systemProperties.page.totalElements" [length]="systemProperties.page.totalElements"
[pageSize]="systemProperties.page.size" [pageSize]="systemProperties.page.size"
(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,65 +55,71 @@
@if (!!timeslots) { @if (!!timeslots) {
<div> <div>
<table mat-table [dataSource]="timeslots.content" matSort (matSortChange)="updateSort($event)"> @if (timeslots.content.length > 0) {
<table mat-table [dataSource]="timeslots.content" matSort (matSortChange)="updateSort($event)">
<ng-container matColumnDef="id"> <ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.timeslots.id' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.timeslots.id' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.id}} </td> <td mat-cell *matCellDef="let timeslot"> {{timeslot.id}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="owner"> <ng-container matColumnDef="owner">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.owner' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.owner' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.owner}} </td> <td mat-cell *matCellDef="let timeslot"> {{timeslot.owner}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="title"> <ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.title' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.title' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.title}} </td> <td mat-cell *matCellDef="let timeslot"> {{timeslot.title}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.type' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.type' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.type}} </td> <td mat-cell *matCellDef="let timeslot"> {{timeslot.type}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="visibility"> <ng-container matColumnDef="visibility">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.visibility' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.visibility' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.visibility}} </td> <td mat-cell *matCellDef="let timeslot"> {{timeslot.visibility}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="start"> <ng-container matColumnDef="start">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.start' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.start' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.start | date:'short'}} </td> <td mat-cell *matCellDef="let timeslot"> {{timeslot.start | date:'short'}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="end"> <ng-container matColumnDef="end">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.end' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{'admin.timeslots.end' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> {{timeslot.end | date:'short'}} </td> <td mat-cell *matCellDef="let timeslot"> {{timeslot.end | date:'short'}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let timeslot"> <td mat-cell *matCellDef="let timeslot">
<button mat-icon-button [title]="'admin.timeslots.edit' | i18n"> <button mat-icon-button [title]="'admin.timeslots.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteTimeslot(timeslot.id)" [title]="'admin.timeslots.delete' | i18n"> <button mat-icon-button (click)="deleteTimeslot(timeslot.id)" [title]="'admin.timeslots.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="timeslots.page.totalElements" [length]="timeslots.page.totalElements"
[pageSize]="timeslots.page.size" [pageSize]="timeslots.page.size"
(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,49 +9,55 @@
@if (!!userAliases) { @if (!!userAliases) {
<div> <div>
<table mat-table [dataSource]="userAliases.content" matSort (matSortChange)="updateSort($event)"> @if (userAliases.content.length > 0) {
<ng-container matColumnDef="id"> <table mat-table [dataSource]="userAliases.content" matSort (matSortChange)="updateSort($event)">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.id' | i18n}}</th> <ng-container matColumnDef="id">
<td mat-cell *matCellDef="let alias">{{alias.id}}</td> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.id' | i18n}}</th>
</ng-container> <td mat-cell *matCellDef="let alias">{{alias.id}}</td>
</ng-container>
<ng-container matColumnDef="alias"> <ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.alias' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.alias' | i18n}}</th>
<td mat-cell *matCellDef="let alias">{{alias.alias}}</td> <td mat-cell *matCellDef="let alias">{{alias.alias}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="target"> <ng-container matColumnDef="target">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.target' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.target' | i18n}}</th>
<td mat-cell *matCellDef="let alias">{{alias.target}}</td> <td mat-cell *matCellDef="let alias">{{alias.target}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="visibility"> <ng-container matColumnDef="visibility">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.visibility' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.user_aliases.visibility' | i18n}}</th>
<td mat-cell *matCellDef="let alias">{{alias.visibility}}</td> <td mat-cell *matCellDef="let alias">{{alias.visibility}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let alias"> <td mat-cell *matCellDef="let alias">
<button mat-icon-button (click)="editAlias(alias)" [title]="'admin.user_aliases.edit' | i18n"> <button mat-icon-button (click)="editAlias(alias)" [title]="'admin.user_aliases.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteAlias(alias)" [title]="'admin.user_aliases.delete' | i18n"> <button mat-icon-button (click)="deleteAlias(alias)" [title]="'admin.user_aliases.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="userAliases.page.totalElements" [length]="userAliases.page.totalElements"
[pageSize]="userAliases.page.size" [pageSize]="userAliases.page.size"
(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>
} }
+44 -38
View File
@@ -9,49 +9,55 @@
@if (!!users) { @if (!!users) {
<div> <div>
<table mat-table [dataSource]="users.content" matSort (matSortChange)="updateSort($event)"> @if (users.content.length > 0) {
<table mat-table [dataSource]="users.content" matSort (matSortChange)="updateSort($event)">
<ng-container matColumnDef="id"> <ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.users.id' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="id"> {{'admin.users.id' | i18n}} </th>
<td mat-cell *matCellDef="let user"> {{user.id}} </td> <td mat-cell *matCellDef="let user"> {{user.id}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="username"> <ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef mat-sort-header="username"> {{'admin.users.username' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="username"> {{'admin.users.username' | i18n}} </th>
<td mat-cell *matCellDef="let user"> {{user.username}} </td> <td mat-cell *matCellDef="let user"> {{user.username}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="status"> <ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header="status"> {{'admin.users.status' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="status"> {{'admin.users.status' | i18n}} </th>
<td mat-cell *matCellDef="let user"> {{user.status}} </td> <td mat-cell *matCellDef="let user"> {{user.status}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th> <th mat-header-cell *matHeaderCellDef> {{'admin.actions' | i18n}} </th>
<td mat-cell *matCellDef="let user"> <td mat-cell *matCellDef="let user">
<button mat-icon-button (click)="editUser(user)" [title]="'admin.users.edit' | i18n"> <button mat-icon-button (click)="editUser(user)" [title]="'admin.users.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button [routerLink]="['/admin/permissions']" [queryParams]="{username: user.username}" <button mat-icon-button [routerLink]="['/admin/permissions']" [queryParams]="{username: user.username}"
[title]="'admin.users.view_permissions' | i18n"> [title]="'admin.users.view_permissions' | i18n">
<mat-icon>security</mat-icon> <mat-icon>security</mat-icon>
</button> </button>
<button mat-icon-button [routerLink]="['/admin/quotas']" [queryParams]="{username: user.username}" <button mat-icon-button [routerLink]="['/admin/quotas']" [queryParams]="{username: user.username}"
[title]="'admin.users.view_quotas' | i18n"> [title]="'admin.users.view_quotas' | i18n">
<mat-icon>data_usage</mat-icon> <mat-icon>data_usage</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteUser(user.username)" [title]="'admin.users.delete' | i18n"> <button mat-icon-button (click)="deleteUser(user.username)" [title]="'admin.users.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<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,54 +9,60 @@
@if (!!voucherMappings) { @if (!!voucherMappings) {
<div> <div>
<table mat-table [dataSource]="voucherMappings.content" matSort (matSortChange)="updateSort($event)"> @if (voucherMappings.content.length > 0) {
<ng-container matColumnDef="id"> <table mat-table [dataSource]="voucherMappings.content" matSort (matSortChange)="updateSort($event)">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.voucher_mappings.id' | i18n}}</th> <ng-container matColumnDef="id">
<td mat-cell *matCellDef="let mapping">{{mapping.id}}</td> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.voucher_mappings.id' | i18n}}</th>
</ng-container> <td mat-cell *matCellDef="let mapping">{{mapping.id}}</td>
</ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.voucher_mappings.name' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'admin.voucher_mappings.name' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.name}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.name}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="voucher"> <ng-container matColumnDef="voucher">
<th mat-header-cell *matHeaderCellDef>{{'admin.voucher_mappings.voucher' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.voucher_mappings.voucher' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.voucher}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.voucher}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="quota"> <ng-container matColumnDef="quota">
<th mat-header-cell *matHeaderCellDef>{{'admin.voucher_mappings.quota' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.voucher_mappings.quota' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.quota}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.quota}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="free"> <ng-container matColumnDef="free">
<th mat-header-cell *matHeaderCellDef>{{'admin.voucher_mappings.free' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.voucher_mappings.free' | i18n}}</th>
<td mat-cell *matCellDef="let mapping">{{mapping.free ? 'Yes' : 'No'}}</td> <td mat-cell *matCellDef="let mapping">{{mapping.free ? 'Yes' : 'No'}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th> <th mat-header-cell *matHeaderCellDef>{{'admin.actions' | i18n}}</th>
<td mat-cell *matCellDef="let mapping"> <td mat-cell *matCellDef="let mapping">
<button mat-icon-button (click)="editMapping(mapping)" [title]="'admin.voucher_mappings.edit' | i18n"> <button mat-icon-button (click)="editMapping(mapping)" [title]="'admin.voucher_mappings.edit' | i18n">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button (click)="deleteMapping(mapping)" [title]="'admin.voucher_mappings.delete' | i18n"> <button mat-icon-button (click)="deleteMapping(mapping)" [title]="'admin.voucher_mappings.delete' | i18n">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</td> </td>
</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;"></tr>
</table> </table>
<mat-paginator <mat-paginator
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[length]="voucherMappings.page.totalElements" [length]="voucherMappings.page.totalElements"
[pageSize]="voucherMappings.page.size" [pageSize]="voucherMappings.page.size"
(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>
} }
+19 -31
View File
@@ -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> <h4>{{'services.category.' + item.key | i18n}}</h4>
@for (item of serviceCategory | keyvalue; track item) { <app-services-grid [services]="item.value"></app-services-grid>
<div> }
<h4>{{'services.category.' + item.key | i18n}}</h4> }
<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> <h4>{{'services.category.' + item.key | i18n}}</h4>
@for (item of serviceCategory | keyvalue; track item) { <app-services-table [services]="item.value"></app-services-table>
<div> }
<h4>{{'services.category.' + item.key | i18n}}</h4> }
<app-services-table [services]="item.value"></app-services-table>
<br>
</div>
}
</div>
}
@@ -8,4 +8,4 @@ header {
mat-button-toggle-group { mat-button-toggle-group {
margin: 8px; margin: 8px;
} }
} }
@@ -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,33 +1,32 @@
<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> </div>
</div> <mat-card-title> <strong>{{'services.' + service.name + '.title' | i18n}}</strong>
<mat-card-title> <strong>{{'services.' + service.name + '.title' | i18n}}</strong> @if (!service.sameSite) {
@if (!service.sameSite) { <mat-icon inline="true">
<mat-icon inline="true"> open_in_new</mat-icon>
open_in_new</mat-icon> }
}
</mat-card-title>
<mat-card-subtitle>{{'services.' + service.name + '.subtitle' | i18n}}</mat-card-subtitle>
</mat-card-header>
</a>
}
@if (!service.url) {
<mat-card-header>
<div class="icon" mat-card-avatar>
<mat-icon>{{'services.' + service.name + '.icon' | i18n}}</mat-icon>
</div>
<mat-card-title> <strong>{{'services.' + service.name + '.title' | i18n}}</strong>
</mat-card-title> </mat-card-title>
<mat-card-subtitle>{{'services.' + service.name + '.subtitle' | i18n}}</mat-card-subtitle> <mat-card-subtitle>{{'services.' + service.name + '.subtitle' | i18n}}</mat-card-subtitle>
</mat-card-header> </mat-card-header>
</a>
}
@if (!service.url) {
<mat-card-header>
<div class="icon" mat-card-avatar>
<mat-icon>{{'services.' + service.name + '.icon' | i18n}}</mat-icon>
</div>
<mat-card-title> <strong>{{'services.' + service.name + '.title' | i18n}}</strong>
</mat-card-title>
<mat-card-subtitle>{{'services.' + service.name + '.subtitle' | i18n}}</mat-card-subtitle>
</mat-card-header>
} }
<mat-card-content> <mat-card-content>
<p> <p>
@@ -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;
+45
View File
@@ -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",
+45
View File
@@ -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",