Add punishment details and history retrieval functionality

This commit introduces a new `DetailsComponent` for displaying detailed punishment data and establishes a route to view punishment history by ID and type. It also updates the API to support fetching individual punishment records and refines database mappings for improved data handling.
This commit is contained in:
2025-04-19 04:02:51 +02:00
parent a1912b96d8
commit 3babde5513
22 changed files with 547 additions and 165 deletions
+4
View File
@@ -45,6 +45,10 @@ export const routes: Routes = [
path: 'bans',
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent)
},
{
path: 'bans/:type/:id',
loadComponent: () => import('./bans/details/details.component').then(m => m.DetailsComponent)
},
{
path: 'economy',
loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent)
+2 -1
View File
@@ -57,7 +57,8 @@
</div>
<div class="historyTable">
<app-history [userType]="userType" [punishmentType]="punishmentType"
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)">
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)"
(selectItem)="setSearch($event)">
</app-history>
</div>
<div class="changePageButtons">
+8
View File
@@ -5,6 +5,7 @@ import {HistoryCount, HistoryService} from '../../api';
import {NgClass, NgForOf, NgIf} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {catchError, map, Observable} from 'rxjs';
import {SearchParams} from './search-terms';
@Component({
selector: 'app-bans',
@@ -210,4 +211,11 @@ export class BansComponent implements OnInit {
this.actualPage = this.page;
}
}
public setSearch(searchParams: SearchParams) {
this.userType = searchParams.userType
this.searchTerm = searchParams.searchTerm
this.punishmentType = searchParams.punishmentType
this.search();
}
}
@@ -0,0 +1,62 @@
<ng-container>
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
[overlay_gradient]="0.5">>
<div class="title" header-content>
<h1>Minecraft Punishments</h1>
</div>
</app-header>
<ng-container *ngIf="punishment === undefined">
<p>Loading...</p>
</ng-container>
<ng-container *ngIf="punishment">
<table [cellSpacing]="0">
<div>
<p>type: {{ this.historyFormat.getType(punishment) }}</p>
<p>is active: {{ this.historyFormat.isActive(punishment) }}</p>
<tbody>
<tr>
<td>Player</td>
<td>
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid)" width="25" height="25"
alt="{{punishment.username}}'s Minecraft skin">
<span class="username">{{ punishment.username }}</span>
</div>
</td>
</tr>
<tr>
<td>Moderator</td>
<td>
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid)" width="25"
height="25"
alt="{{punishment.punishedBy}}'s Minecraft skin">
<span class="username">{{ punishment.punishedBy }}</span>
</div>
</td>
</tr>
<tr>
<td>Reason</td>
<td>{{ punishment.reason | removeTrailingPeriod }}</td>
</tr>
<tr>
<td>Date</td>
<td>{{ this.historyFormat.getPunishmentTime(punishment) }}</td>
</tr>
<tr>
<td>Expires</td>
<td>{{ this.historyFormat.getExpiredTime(punishment) }}</td>
</tr>
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
<tr>
<td>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</td>
<td>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</td>
</tr>
</ng-container>
</tbody>
</div>
</table>
</ng-container>
</ng-container>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DetailsComponent } from './details.component';
describe('DetailsComponent', () => {
let component: DetailsComponent;
let fixture: ComponentFixture<DetailsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DetailsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,69 @@
import {Component, OnInit} from '@angular/core';
import {HistoryService, PunishmentHistory} from '../../../api';
import {NgIf, NgOptimizedImage} from '@angular/common';
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
import {HistoryFormatService} from '../history-format.service';
import {ActivatedRoute} from '@angular/router';
import {catchError, map} from 'rxjs';
import {HeaderComponent} from '../../header/header.component';
@Component({
selector: 'app-details',
imports: [
NgIf,
NgOptimizedImage,
RemoveTrailingPeriodPipe,
HeaderComponent
],
templateUrl: './details.component.html',
styleUrl: './details.component.scss'
})
export class DetailsComponent implements OnInit {
type: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
id: number = -1;
punishment: PunishmentHistory | undefined;
constructor(private route: ActivatedRoute,
private historyApi: HistoryService,
public historyFormat: HistoryFormatService) {
}
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
switch (params.get('type')) {
case 'ban':
this.type = 'ban';
break;
case 'mute':
this.type = 'mute';
break;
case 'kick':
this.type = 'kick';
break;
case 'warn':
this.type = 'warn';
break;
default:
throw new Error("Invalid type");
}
this.id = Number(params.get('id') || '-1');
this.loadPunishmentData();
});
}
private loadPunishmentData() {
if (!this.type || this.type === 'all' || this.id < 0 || isNaN(this.id)) {
console.error("Invalid type or id");
return;
}
this.historyApi.getHistoryById(this.type, this.id).pipe(
map(punishment => {
this.punishment = punishment;
}),
catchError(err => {
console.error(err);
return [];
})
).subscribe();
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { HistoryFormatService } from './history-format.service';
describe('HistoryFormatService', () => {
let service: HistoryFormatService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(HistoryFormatService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,58 @@
import {Injectable} from '@angular/core';
import {PunishmentHistory} from '../../api';
@Injectable({
providedIn: 'root'
})
export class HistoryFormatService {
constructor() {
}
public getPunishmentTime(entry: PunishmentHistory) {
const date = new Date(entry.punishmentTime);
return date.toLocaleString(navigator.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
public isActive(entry: PunishmentHistory): boolean {
return entry.expiryTime > Date.now();
}
public getType(entry: PunishmentHistory): string {
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
}
public getExpiredTime(entry: PunishmentHistory): string {
let suffix: string = '';
if (entry.removedBy !== null && entry.removedBy !== undefined) {
suffix = '(' + entry.removedBy + ')';
}
if (entry.expiryTime <= 0) {
return "Permanent " + this.getType(entry) + " " + suffix;
}
const date = new Date(entry.expiryTime);
return date.toLocaleString(navigator.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
}) + " " + suffix;
}
public getAvatarUrl(entry: string): string {
let uuid = entry.replace('-', '');
if (uuid === 'C') {
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
}
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
}
}
@@ -19,24 +19,32 @@
<div>
<tbody>
<tr class="historyPlayerRow" *ngFor="let entry of history">
<td class="historyType">{{ getType(entry) }}</td>
<td class="historyPlayer">
<td class="historyType" (click)="showDetailedPunishment(entry)">
{{ this.historyFormat.getType(entry) }}
</td>
<td class="historyPlayer" (click)="setSearch(entry.type, entry.username, 'player')">
<div class="playerContainer">
<img class="avatar" [ngSrc]="getAvatarUrl(entry.uuid)" width="25" height="25"
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.uuid)" width="25" height="25"
alt="{{entry.username}}'s Minecraft skin">
<span class="username">{{ entry.username }}</span>
</div>
</td>
<td class="historyPlayer">
<td class="historyPlayer" (click)="setSearch(entry.type, entry.punishedBy, 'staff')">
<div class="playerContainer">
<img class="avatar" [ngSrc]="getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
alt="{{entry.punishedBy}}'s Minecraft skin">
<span>{{ entry.punishedBy }}</span>
</div>
</td>
<td class="historyReason">{{ entry.reason | removeTrailingPeriod }}</td>
<td class="historyDate">{{ getPunishmentTime(entry) }}</td>
<td class="historyDate">{{ getExpiredTime(entry) }}</td>
<td class="historyReason" (click)="showDetailedPunishment(entry)">
{{ entry.reason | removeTrailingPeriod }}
</td>
<td class="historyDate" (click)="showDetailedPunishment(entry)">
{{ this.historyFormat.getPunishmentTime(entry) }}
</td>
<td class="historyDate" (click)="showDetailedPunishment(entry)">
{{ this.historyFormat.getExpiredTime(entry) }}
</td>
</tr>
</tbody>
</div>
@@ -62,6 +62,7 @@ table tr td {
.historyPlayerRow:hover {
background-color: var(--history-table-row-color);
cursor: pointer;
}
.historyTableHead {
@@ -1,11 +1,14 @@
import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core';
import {BASE_PATH, HistoryService, PunishmentHistoryInner} from '../../../api';
import {BASE_PATH, HistoryService, PunishmentHistory} from '../../../api';
import {catchError, map, Observable, shareReplay} from 'rxjs';
import {NgForOf, NgIf, NgOptimizedImage} from '@angular/common';
import {CookieService} from 'ngx-cookie-service';
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
import {HttpErrorResponse} from '@angular/common/http';
import {environment} from '../../../environments/environment';
import {HistoryFormatService} from '../history-format.service';
import {SearchParams} from '../search-terms';
import {Router} from '@angular/router';
@Component({
selector: 'app-history',
@@ -30,12 +33,13 @@ export class HistoryComponent implements OnInit, OnChanges {
@Input() searchTerm: string = '';
@Output() pageChange = new EventEmitter<number>();
@Output() selectItem = new EventEmitter<SearchParams>();
private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
public history: PunishmentHistoryInner[] = []
public history: PunishmentHistory[] = []
constructor(private historyApi: HistoryService) {
constructor(private historyApi: HistoryService, public historyFormat: HistoryFormatService, private router: Router) {
}
ngOnChanges(): void {
@@ -47,7 +51,7 @@ export class HistoryComponent implements OnInit, OnChanges {
}
private reloadHistory(): void {
let historyObservable: Observable<PunishmentHistoryInner[]>;
let historyObservable: Observable<PunishmentHistory[]>;
if (this.searchTerm.length === 0) {
historyObservable = this.historyApi.getHistoryForAll(this.userType, this.punishmentType, this.page);
} else {
@@ -89,42 +93,31 @@ export class HistoryComponent implements OnInit, OnChanges {
).subscribe();
}
public getPunishmentTime(entry: PunishmentHistoryInner) {
const date = new Date(entry.punishmentTime);
return date.toLocaleString(navigator.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
public getType(entry: PunishmentHistoryInner) {
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
}
public getExpiredTime(entry: PunishmentHistoryInner) {
if (entry.expiryTime <= 0) {
return "Permanent " + this.getType(entry);
setSearch(type: PunishmentHistory.TypeEnum, name: string, userType: 'player' | 'staff') {
let punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
switch (type) {
case 'ban':
punishmentType = 'ban';
break;
case 'mute':
punishmentType = 'mute';
break;
case 'kick':
punishmentType = 'kick';
break;
case 'warn':
punishmentType = 'warn';
break;
}
const date = new Date(entry.expiryTime);
return date.toLocaleString(navigator.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
let searchParams: SearchParams = {
userType: userType,
punishmentType: punishmentType,
searchTerm: name
}
this.selectItem.emit(searchParams);
}
public getAvatarUrl(entry: string): string {
let uuid = entry.replace('-', '');
if (uuid === 'C') {
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
}
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
public showDetailedPunishment(entry: PunishmentHistory) {
this.router.navigate([`bans/${entry.type}/${entry.id}`]).then();
}
}
+5
View File
@@ -0,0 +1,5 @@
export interface SearchParams {
userType: 'player' | 'staff';
searchTerm: string;
punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn';
}