diff --git a/Api/Controllers/v1/VsDuelParticipantsController.cs b/Api/Controllers/v1/VsDuelParticipantsController.cs index 700ee16..d6872dc 100644 --- a/Api/Controllers/v1/VsDuelParticipantsController.cs +++ b/Api/Controllers/v1/VsDuelParticipantsController.cs @@ -3,7 +3,6 @@ using Application.Errors; using Application.Interfaces; using Asp.Versioning; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Api.Controllers.v1 @@ -36,5 +35,25 @@ namespace Api.Controllers.v1 return StatusCode(StatusCodes.Status500InternalServerError); } } + + [HttpGet("Player/{playerId:guid}")] + public async Task>> GetPlayerVsDuelParticipants(Guid playerId, [FromQuery] int last, + CancellationToken cancellationToken) + { + try + { + var numberOfParticipationResult = + await vsDuelParticipantRepository.GetVsDuelParticipantDetailsAsync(playerId, last, + cancellationToken); + return numberOfParticipationResult.IsFailure + ? BadRequest(numberOfParticipationResult.Error) + : Ok(numberOfParticipationResult.Value); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } } } diff --git a/Application/DataTransferObjects/VsDuelParticipant/VsDuelParticipantDetailDto.cs b/Application/DataTransferObjects/VsDuelParticipant/VsDuelParticipantDetailDto.cs new file mode 100644 index 0000000..6076507 --- /dev/null +++ b/Application/DataTransferObjects/VsDuelParticipant/VsDuelParticipantDetailDto.cs @@ -0,0 +1,10 @@ +namespace Application.DataTransferObjects.VsDuelParticipant; + +public class VsDuelParticipantDetailDto +{ + public Guid PlayerId { get; set; } + + public DateTime EventDate { get; set; } + + public long WeeklyPoints { get; set; } +} \ No newline at end of file diff --git a/Application/Interfaces/IVsDuelParticipantRepository.cs b/Application/Interfaces/IVsDuelParticipantRepository.cs index b4ac498..af220bb 100644 --- a/Application/Interfaces/IVsDuelParticipantRepository.cs +++ b/Application/Interfaces/IVsDuelParticipantRepository.cs @@ -6,4 +6,7 @@ namespace Application.Interfaces; public interface IVsDuelParticipantRepository { Task> UpdateVsDuelParticipant(VsDuelParticipantDto vsDuelParticipantDto, CancellationToken cancellationToken); + + Task>> GetVsDuelParticipantDetailsAsync(Guid playerId, int last, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Application/Profiles/VsDuelParticipantProfile.cs b/Application/Profiles/VsDuelParticipantProfile.cs index 2cce107..db5accc 100644 --- a/Application/Profiles/VsDuelParticipantProfile.cs +++ b/Application/Profiles/VsDuelParticipantProfile.cs @@ -12,5 +12,8 @@ public class VsDuelParticipantProfile : Profile .ForMember(des => des.PlayerName, opt => opt.MapFrom(src => src.Player.PlayerName)); CreateMap(); + + CreateMap() + .ForMember(des => des.EventDate, opt => opt.MapFrom(src => src.VsDuel.EventDate)); } } \ No newline at end of file diff --git a/Application/Repositories/VsDuelParticipantRepository.cs b/Application/Repositories/VsDuelParticipantRepository.cs index f170a3e..1e857e1 100644 --- a/Application/Repositories/VsDuelParticipantRepository.cs +++ b/Application/Repositories/VsDuelParticipantRepository.cs @@ -3,6 +3,7 @@ using Application.DataTransferObjects.VsDuelParticipant; using Application.Errors; using Application.Interfaces; using AutoMapper; +using AutoMapper.QueryableExtensions; using Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -31,4 +32,17 @@ public class VsDuelParticipantRepository(ApplicationContext context, IMapper map return Result.Failure(GeneralErrors.DatabaseError); } } + + public async Task>> GetVsDuelParticipantDetailsAsync(Guid playerId, int last, CancellationToken cancellationToken) + { + var vsDuelPlayersParticipants = await context.VsDuelParticipants + .ProjectTo(mapper.ConfigurationProvider) + .AsNoTracking() + .OrderByDescending(e => e.EventDate) + .Where(e => e.PlayerId == playerId) + .Take(last) + .ToListAsync(cancellationToken); + + return Result.Success(vsDuelPlayersParticipants); + } } \ No newline at end of file diff --git a/PlayerManagement.sln b/PlayerManagement.sln index ac6ba05..ca50e93 100644 --- a/PlayerManagement.sln +++ b/PlayerManagement.sln @@ -13,6 +13,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "Application\ EndProject Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "Ui", "Ui\Ui.esproj", "{77168004-AF1E-4F51-A096-E8E8BF18A37A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Projektmappenelemente", "Projektmappenelemente", "{E8BE2EF5-0D8D-4D68-9979-0680938F26DF}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/README.md b/README.md index 3b8cd0f..27c5d93 100644 --- a/README.md +++ b/README.md @@ -1 +1,96 @@ -# PlayerManagement \ No newline at end of file +# PlayerManagement +## Changelog + +All notable changes to this project are documented here. +This project is currently in the **Beta Phase**. + +--- + +### **[0.5.0-beta]** - *2025-01-21* +#### ✨ Added +- **Player VS Duel**: In the player detail view, the VS points can now be viewed as a bar chart. + +#### 🛠️ Fixed +- *(N/A)* + +--- + +### **[0.4.1-beta]** - *2025-01-21* +#### ✨ Added +- **Excel Import**: Players can now be imported via Excel. + +#### 🛠️ Fixed +- **Week Pipe Logic**: Corrected calculation logic for weekly processing. + +--- + +### **[0.4.0-beta]** - *2025-01-20* +#### ✨ Added +- **Player Dismissal Page**: A new GUI page was added for dismissing players. + +#### 🛠️ Fixed +- *(N/A)* + +--- + +### **[0.3.6-beta]** - *2025-01-16* +#### ✨ Added +- **Player Dismissal Function**: Core dismissal functionality implemented. + +#### 🛠️ Fixed +- *(N/A)* + +--- + +### **[0.3.5-beta]** - *2025-01-09* +#### ✨ Added +- *(N/A)* + +#### 🛠️ Fixed +- **MVP Formula**: Corrected MVP calculation formula. + +--- + +### **[0.3.4-beta]** - *2025-01-07* +#### ✨ Added +- **Alliance MVP Calculation**: Implemented MVP calculation for alliances with API endpoint support. + +#### 🛠️ Fixed +- *(N/A)* + +--- + +### **[0.3.3-beta]** - *2024-12-17* +#### ✨ Added +- **Custom Event**: Introduced custom event functionality. + +#### 🛠️ Fixed +- *(N/A)* + +--- + +### **[0.3.2-beta]** - *2024-12-03* +#### ✨ Added +- **Event Progress**: "In Progress" status added to events. +- **League Details in VS Duel**: Added league tiers (e.g., Silver, Gold, Diamond). + +#### 🛠️ Fixed +- *(N/A)* + +--- + +### **[0.3.1-beta]** - *2024-11-28* +#### ✨ Added +- **Zombie Siege Event**: Introduced a new Zombie Siege event. + +#### 🛠️ Fixed +- *(N/A)* + +--- + +### **[0.3.0-beta]** - *2024-11-26* +#### ✨ Added +- **Initial Beta Release**: Core features introduced in the first beta release. + +#### 🛠️ Fixed +- *(N/A in the initial release)* diff --git a/Ui/package-lock.json b/Ui/package-lock.json index 751421a..b12ec95 100644 --- a/Ui/package-lock.json +++ b/Ui/package-lock.json @@ -20,6 +20,7 @@ "@ng-bootstrap/ng-bootstrap": "^17.0.1", "@popperjs/core": "^2.11.8", "@sweetalert2/theme-dark": "^5.0.18", + "ag-charts-angular": "^11.0.4", "bootstrap": "^5.3.2", "bootstrap-icons": "^1.11.3", "bootswatch": "^5.3.3", @@ -5226,6 +5227,41 @@ "node": ">=8.9.0" } }, + "node_modules/ag-charts-angular": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/ag-charts-angular/-/ag-charts-angular-11.0.4.tgz", + "integrity": "sha512-1D9xEky6RcwJ49SLGScrVyJNOIy+wH32CaEY4JTdS36QqgFcOVwm5QTiMf/zTa+Qsbz4HfRVBkYpBQuMR0s2Cw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0", + "ag-charts-community": "11.0.4" + } + }, + "node_modules/ag-charts-community": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/ag-charts-community/-/ag-charts-community-11.0.4.tgz", + "integrity": "sha512-TFShWfZaA1yJ/hb3jwtNAqBG2Qy9VvoQ9mWLr29ilT6+2R2e30RSk2oH7FAQ2l5nS1367dRf137Td9tUTTuVLg==", + "peer": true, + "dependencies": { + "ag-charts-locale": "11.0.4", + "ag-charts-types": "11.0.4" + } + }, + "node_modules/ag-charts-locale": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/ag-charts-locale/-/ag-charts-locale-11.0.4.tgz", + "integrity": "sha512-xCQA8CtcUyqU4qYg7u9oUQ+SghIsWAQvY60Bu0ghJJ/bDeW8+ptEU0ogBapuDqfb9B9kqWKgASxbX6diqb+HVQ==", + "peer": true + }, + "node_modules/ag-charts-types": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-11.0.4.tgz", + "integrity": "sha512-K/Mi7FXvSCoABLSrqQ70k1QrIL5R6RNCt2NAppOxMEir+DVFPqKZtghruobc2MGVUUKkT9MCn6Dun+fL6yZjfA==", + "peer": true + }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", diff --git a/Ui/package.json b/Ui/package.json index 371742a..c805270 100644 --- a/Ui/package.json +++ b/Ui/package.json @@ -22,6 +22,7 @@ "@ng-bootstrap/ng-bootstrap": "^17.0.1", "@popperjs/core": "^2.11.8", "@sweetalert2/theme-dark": "^5.0.18", + "ag-charts-angular": "^11.0.4", "bootstrap": "^5.3.2", "bootstrap-icons": "^1.11.3", "bootswatch": "^5.3.3", diff --git a/Ui/src/app/app.module.ts b/Ui/src/app/app.module.ts index 297ca61..45d1375 100644 --- a/Ui/src/app/app.module.ts +++ b/Ui/src/app/app.module.ts @@ -27,7 +27,6 @@ import {jwtInterceptor} from "./interceptors/jwt.interceptor"; import { PlayerNoteModalComponent } from './modals/player-note-modal/player-note-modal.component'; import { PlayerAdmonitionModalComponent } from './modals/player-admonition-modal/player-admonition-modal.component'; import { PlayerInfoMarshalGuardComponent } from './pages/player-information/player-info-marshal-guard/player-info-marshal-guard.component'; -import { PlayerInfoVsDuelComponent } from './pages/player-information/player-info-vs-duel/player-info-vs-duel.component'; import { WeekPipe } from './helpers/week.pipe'; import { PlayerInfoDesertStormComponent } from './pages/player-information/player-info-desert-storm/player-info-desert-storm.component'; import { PlayerInfoCustomEventComponent } from './pages/player-information/player-info-custom-event/player-info-custom-event.component'; @@ -55,6 +54,8 @@ import { CustomEventDetailComponent } from './pages/custom-event/custom-event-de import { DismissPlayerComponent } from './pages/dismiss-player/dismiss-player.component'; import { PlayerDismissInformationModalComponent } from './modals/player-dismiss-information-modal/player-dismiss-information-modal.component'; import { PlayerExcelImportModalComponent } from './modals/player-excel-import-modal/player-excel-import-modal.component'; +import {AgCharts} from "ag-charts-angular"; +import {PlayerInfoVsDuelComponent} from "./pages/player-information/player-info-vs-duel/player-info-vs-duel.component"; @NgModule({ declarations: [ @@ -73,10 +74,10 @@ import { PlayerExcelImportModalComponent } from './modals/player-excel-import-mo PlayerNoteModalComponent, PlayerAdmonitionModalComponent, PlayerInfoMarshalGuardComponent, - PlayerInfoVsDuelComponent, WeekPipe, PlayerInfoDesertStormComponent, PlayerInfoCustomEventComponent, + PlayerInfoVsDuelComponent, VsDuelCreateModalComponent, VsDuelDetailComponent, VsDuelEditComponent, @@ -102,25 +103,26 @@ import { PlayerExcelImportModalComponent } from './modals/player-excel-import-mo PlayerDismissInformationModalComponent, PlayerExcelImportModalComponent ], - imports: [ - BrowserModule, - AppRoutingModule, - BrowserAnimationsModule, - NgbModule, - FormsModule, - NgxPaginationModule, - ReactiveFormsModule, - NgxSpinnerModule, - NgbRatingModule, - ToastrModule.forRoot({ - positionClass: 'toast-bottom-right', - }), - JwtModule.forRoot({ - config: { - tokenGetter: () => localStorage.getItem(''), - } - }) - ], + imports: [ + BrowserModule, + AppRoutingModule, + BrowserAnimationsModule, + NgbModule, + FormsModule, + NgxPaginationModule, + ReactiveFormsModule, + NgxSpinnerModule, + NgbRatingModule, + ToastrModule.forRoot({ + positionClass: 'toast-bottom-right', + }), + JwtModule.forRoot({ + config: { + tokenGetter: () => localStorage.getItem(''), + } + }), + AgCharts + ], providers: [ provideHttpClient(withInterceptors([spinnerInterceptor, jwtInterceptor])) ], diff --git a/Ui/src/app/models/vsDuelParticipant.model.ts b/Ui/src/app/models/vsDuelParticipant.model.ts index 35aeb0e..423afac 100644 --- a/Ui/src/app/models/vsDuelParticipant.model.ts +++ b/Ui/src/app/models/vsDuelParticipant.model.ts @@ -5,3 +5,9 @@ export interface VsDuelParticipantModel { weeklyPoints: number; playerName: string; } + +export interface VsDuelParticipantDetailModel { + playerId: string; + eventDate: Date; + weeklyPoints: number; +} diff --git a/Ui/src/app/navigation/navigation.component.html b/Ui/src/app/navigation/navigation.component.html index a7fadd3..fe8eb7d 100644 --- a/Ui/src/app/navigation/navigation.component.html +++ b/Ui/src/app/navigation/navigation.component.html @@ -6,7 +6,7 @@ {{loggedInUser.allianceName}} - v.{{version}} + v.{{version}} + + + + @if (vsDuelsLoaded) { + + } + + diff --git a/Ui/src/app/pages/player-information/player-info-vs-duel/player-info-vs-duel.component.ts b/Ui/src/app/pages/player-information/player-info-vs-duel/player-info-vs-duel.component.ts index e718777..29f4967 100644 --- a/Ui/src/app/pages/player-information/player-info-vs-duel/player-info-vs-duel.component.ts +++ b/Ui/src/app/pages/player-information/player-info-vs-duel/player-info-vs-duel.component.ts @@ -1,10 +1,92 @@ -import {Component} from '@angular/core'; +import {Component, inject, Input} from '@angular/core'; +import { AgChartOptions } from "ag-charts-community"; +import {DatePipe} from "@angular/common"; +import {VsDuelParticipantService} from "../../../services/vs-duel-participant.service"; +import {ToastrService} from "ngx-toastr"; + @Component({ selector: 'app-player-info-vs-duel', templateUrl: './player-info-vs-duel.component.html', - styleUrl: './player-info-vs-duel.component.css' + styleUrl: './player-info-vs-duel.component.css', + providers: [DatePipe] }) export class PlayerInfoVsDuelComponent { + @Input({required: true}) playerId!: string; + + private readonly _vsDuelParticipantsService: VsDuelParticipantService = inject(VsDuelParticipantService); + private readonly _datePipe: DatePipe = inject(DatePipe); + private readonly _toastr: ToastrService = inject(ToastrService); + + numberOfLoadVsDuels: number = 10; + vsDuelsLoaded: boolean = false; + + + options: AgChartOptions = { + title: { + text: 'Weekly Points' + }, + data: [], + series: [ + { + type: 'bar', + xKey: 'date', + xName: 'Date', + yKey: 'points', + yName: 'Weekly Points', + stacked: false, + fill: 'blue' + } + ], + axes: [ + { + type: 'number', + position: 'left', + label: { + formatter: (params: any) => { + return params.value.toLocaleString('en-US') + } + } + }, + { + type: 'category', + position: 'bottom', + } + ] + } + + getData(take: number) { + const chartData: {date: string, points: number}[] = []; + this._vsDuelParticipantsService.getVsDuelParticipantsDetail(this.playerId, take).subscribe({ + next: ((response) => { + if (response) { + this.vsDuelsLoaded = true; + if (response.length < take) { + this._toastr.info('Fewer vs duels were held than wanted to be loaded'); + this.numberOfLoadVsDuels = response.length; + } + response.forEach(player => { + const data: {date: string, points: number} = { + date: this._datePipe.transform(player.eventDate, 'dd.MM.yyyy')!, + points: player.weeklyPoints + }; + chartData.push(data) + }) + this.options = { + ...this.options, + data: chartData.reverse() + } + } + }), + error: error => { + console.log(error); + } + }) + } + + + onReloadVsDuels() { + this.getData(this.numberOfLoadVsDuels); + } } diff --git a/Ui/src/app/pages/player-information/player-information.component.html b/Ui/src/app/pages/player-information/player-information.component.html index 200766f..9cf70f2 100644 --- a/Ui/src/app/pages/player-information/player-information.component.html +++ b/Ui/src/app/pages/player-information/player-information.component.html @@ -59,7 +59,7 @@
- +
diff --git a/Ui/src/app/pages/player/player.component.html b/Ui/src/app/pages/player/player.component.html index f267548..dad5b83 100644 --- a/Ui/src/app/pages/player/player.component.html +++ b/Ui/src/app/pages/player/player.component.html @@ -58,7 +58,7 @@ {{player.playerName}}
- +
@@ -66,8 +66,8 @@ {{player.rankName}}
- - + +
diff --git a/Ui/src/app/pages/vs-duel/vs-duel.component.html b/Ui/src/app/pages/vs-duel/vs-duel.component.html index 51aa177..f9b732f 100644 --- a/Ui/src/app/pages/vs-duel/vs-duel.component.html +++ b/Ui/src/app/pages/vs-duel/vs-duel.component.html @@ -64,9 +64,9 @@ }
- - - + + +
diff --git a/Ui/src/app/pages/zombie-siege/zombie-siege.component.html b/Ui/src/app/pages/zombie-siege/zombie-siege.component.html index e566872..5acb017 100644 --- a/Ui/src/app/pages/zombie-siege/zombie-siege.component.html +++ b/Ui/src/app/pages/zombie-siege/zombie-siege.component.html @@ -92,9 +92,9 @@ {{zombieSiege.createdBy}}
- - - + + +
diff --git a/Ui/src/app/services/vs-duel-participant.service.ts b/Ui/src/app/services/vs-duel-participant.service.ts index 67cccd7..c59f590 100644 --- a/Ui/src/app/services/vs-duel-participant.service.ts +++ b/Ui/src/app/services/vs-duel-participant.service.ts @@ -1,8 +1,9 @@ import {inject, Injectable} from '@angular/core'; import {environment} from "../../environments/environment"; -import {HttpClient} from "@angular/common/http"; -import {VsDuelParticipantModel} from "../models/vsDuelParticipant.model"; +import {HttpClient, HttpParams} from "@angular/common/http"; +import {VsDuelParticipantDetailModel, VsDuelParticipantModel} from "../models/vsDuelParticipant.model"; import {Observable} from "rxjs"; +import {MarshalGuardParticipantModel} from "../models/marshalGuardParticipant.model"; @Injectable({ providedIn: 'root' @@ -12,6 +13,13 @@ export class VsDuelParticipantService { private readonly _serviceUrl = environment.apiBaseUrl + 'VsDuelParticipants/'; private readonly _httpClient: HttpClient = inject(HttpClient); + + getVsDuelParticipantsDetail(playerId: string, last: number): Observable { + let params = new HttpParams(); + params = params.append('last', last); + return this._httpClient.get(this._serviceUrl + 'Player/' + playerId, {params: params}); + } + updateVsDuelParticipant(vsDuelParticipantId: string, vsDuelParticipant: VsDuelParticipantModel): Observable { return this._httpClient.put(this._serviceUrl + vsDuelParticipantId, vsDuelParticipant); }