From bb735a68aa70ed636fc3cd430af37533564338a0 Mon Sep 17 00:00:00 2001 From: Tomasi - Developing Date: Tue, 17 Dec 2024 11:27:47 +0100 Subject: [PATCH] Implement custom event --- .../v1/CustomEventParticipantsController.cs | 112 ++++++++ Application/Application.csproj | 1 - Application/ApplicationDependencyInjection.cs | 1 + .../CustomEvent/CreateCustomEventDto.cs | 2 +- .../CustomEvent/UpdateCustomEventDto.cs | 2 +- .../CreateCustomEventParticipantDto.cs | 16 ++ .../UpdateCustomEventParticipantDto.cs | 18 ++ .../ICustomEventParticipantRepository.cs | 17 ++ .../Profiles/CustomEventParticipantProfile.cs | 6 +- Application/Profiles/CustomEventProfile.cs | 7 +- .../CustomEventParticipantRepository.cs | 83 ++++++ Ui/src/app/app-routing.module.ts | 2 + Ui/src/app/app.module.ts | 6 +- ...tom-event-participants-model.component.css | 9 + ...om-event-participants-model.component.html | 68 +++++ ...stom-event-participants-model.component.ts | 75 ++++++ Ui/src/app/models/customEvent.model.ts | 17 +- .../models/customEventParticipant.model.ts | 4 +- .../custom-event-detail.component.css | 0 .../custom-event-detail.component.html | 67 +++++ .../custom-event-detail.component.ts | 48 ++++ .../custom-event/custom-event.component.html | 151 ++++++++++- .../custom-event/custom-event.component.ts | 251 +++++++++++++++++- .../custom-event-participant.service.ts | 23 ++ Ui/src/app/services/custom-event.service.ts | 38 +++ 25 files changed, 1010 insertions(+), 14 deletions(-) create mode 100644 Api/Controllers/v1/CustomEventParticipantsController.cs create mode 100644 Application/DataTransferObjects/CustomEventParticipant/CreateCustomEventParticipantDto.cs create mode 100644 Application/DataTransferObjects/CustomEventParticipant/UpdateCustomEventParticipantDto.cs create mode 100644 Application/Interfaces/ICustomEventParticipantRepository.cs create mode 100644 Application/Repositories/CustomEventParticipantRepository.cs create mode 100644 Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.css create mode 100644 Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.html create mode 100644 Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.ts create mode 100644 Ui/src/app/pages/custom-event/custom-event-detail/custom-event-detail.component.css create mode 100644 Ui/src/app/pages/custom-event/custom-event-detail/custom-event-detail.component.html create mode 100644 Ui/src/app/pages/custom-event/custom-event-detail/custom-event-detail.component.ts create mode 100644 Ui/src/app/services/custom-event-participant.service.ts create mode 100644 Ui/src/app/services/custom-event.service.ts diff --git a/Api/Controllers/v1/CustomEventParticipantsController.cs b/Api/Controllers/v1/CustomEventParticipantsController.cs new file mode 100644 index 0000000..5bf8450 --- /dev/null +++ b/Api/Controllers/v1/CustomEventParticipantsController.cs @@ -0,0 +1,112 @@ +using Application.DataTransferObjects.CustomEventParticipant; +using Application.Interfaces; +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers.v1 +{ + [Route("api/v{version:apiVersion}/[controller]")] + [ApiController] + [ApiVersion("1.0")] + [Authorize] + public class CustomEventParticipantsController(ICustomEventParticipantRepository customEventParticipantRepository, ILogger logger) : ControllerBase + { + + [HttpGet("{customEventParticipantId:guid}")] + public async Task> GetCustomEventParticipant( + Guid customEventParticipantId, CancellationToken cancellationToken) + { + try + { + var customEventParticipantResult = + await customEventParticipantRepository.GetCustomEventParticipantAsync(customEventParticipantId, + cancellationToken); + + return customEventParticipantResult.IsFailure + ? BadRequest(customEventParticipantResult.Error) + : Ok(customEventParticipantResult.Value); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpGet("Player/{playerId:guid}")] + public async Task>> GetPlayerCustomEventParticipants( + Guid playerId, [FromQuery] int last, + CancellationToken cancellationToken) + { + try + { + var customEventPlayerParticipatedResult = + await customEventParticipantRepository.GetPlayerCustomEventParticipantsAsync(playerId, last, + cancellationToken); + + if (customEventPlayerParticipatedResult.IsFailure) + return BadRequest(customEventPlayerParticipatedResult.Error); + + return customEventPlayerParticipatedResult.Value.Count > 0 + ? Ok(customEventPlayerParticipatedResult.Value) + : NoContent(); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpPost] + public async Task InsertCustomEventParticipant( + List createCustomEventParticipants, CancellationToken cancellationToken) + { + try + { + if (!ModelState.IsValid) return UnprocessableEntity(ModelState); + + var createResult = + await customEventParticipantRepository.InsertCustomEventParticipantAsync( + createCustomEventParticipants, cancellationToken); + + return createResult.IsFailure + ? BadRequest(createResult.Error) + : StatusCode(StatusCodes.Status201Created); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpPut("{customEventParticipantId:guid}")] + public async Task> UpdateCustomEventParticipant( + Guid customEventParticipantId, + UpdateCustomEventParticipantDto updateEventParticipantDto, CancellationToken cancellationToken) + { + try + { + if (!ModelState.IsValid) return UnprocessableEntity(ModelState); + + if (customEventParticipantId != updateEventParticipantDto.Id) + return Conflict(new Error("", "")); + + var updateResult = + await customEventParticipantRepository.UpdateCustomEventParticipantAsync( + updateEventParticipantDto, cancellationToken); + + return updateResult.IsFailure + ? BadRequest(updateResult.Error) + : Ok(updateResult.Value); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/Application/Application.csproj b/Application/Application.csproj index c83acf7..70ac2aa 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -10,7 +10,6 @@ - diff --git a/Application/ApplicationDependencyInjection.cs b/Application/ApplicationDependencyInjection.cs index dc3cc75..6e87cd6 100644 --- a/Application/ApplicationDependencyInjection.cs +++ b/Application/ApplicationDependencyInjection.cs @@ -24,6 +24,7 @@ public static class ApplicationDependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Application/DataTransferObjects/CustomEvent/CreateCustomEventDto.cs b/Application/DataTransferObjects/CustomEvent/CreateCustomEventDto.cs index 5ada09b..09c25c0 100644 --- a/Application/DataTransferObjects/CustomEvent/CreateCustomEventDto.cs +++ b/Application/DataTransferObjects/CustomEvent/CreateCustomEventDto.cs @@ -22,7 +22,7 @@ public class CreateCustomEventDto public Guid AllianceId { get; set; } [Required] - public required string EventDateString { get; set; } + public required string EventDate { get; set; } public bool IsInProgress { get; set; } } \ No newline at end of file diff --git a/Application/DataTransferObjects/CustomEvent/UpdateCustomEventDto.cs b/Application/DataTransferObjects/CustomEvent/UpdateCustomEventDto.cs index 687472f..183d17d 100644 --- a/Application/DataTransferObjects/CustomEvent/UpdateCustomEventDto.cs +++ b/Application/DataTransferObjects/CustomEvent/UpdateCustomEventDto.cs @@ -22,7 +22,7 @@ public class UpdateCustomEventDto public required string Description { get; set; } [Required] - public required string EventDateString { get; set; } + public required string EventDate { get; set; } [Required] public bool IsInProgress { get; set; } diff --git a/Application/DataTransferObjects/CustomEventParticipant/CreateCustomEventParticipantDto.cs b/Application/DataTransferObjects/CustomEventParticipant/CreateCustomEventParticipantDto.cs new file mode 100644 index 0000000..f730b08 --- /dev/null +++ b/Application/DataTransferObjects/CustomEventParticipant/CreateCustomEventParticipantDto.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace Application.DataTransferObjects.CustomEventParticipant; + +public class CreateCustomEventParticipantDto +{ + [Required] + public Guid CustomEventId { get; set; } + + [Required] + public Guid PlayerId { get; set; } + + public bool? Participated { get; set; } + + public long? AchievedPoints { get; set; } +} \ No newline at end of file diff --git a/Application/DataTransferObjects/CustomEventParticipant/UpdateCustomEventParticipantDto.cs b/Application/DataTransferObjects/CustomEventParticipant/UpdateCustomEventParticipantDto.cs new file mode 100644 index 0000000..645c350 --- /dev/null +++ b/Application/DataTransferObjects/CustomEventParticipant/UpdateCustomEventParticipantDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Application.DataTransferObjects.CustomEventParticipant; + +public class UpdateCustomEventParticipantDto +{ + [Required] public Guid Id { get; set; } + + [Required] + public Guid CustomEventId { get; set; } + + [Required] + public Guid PlayerId { get; set; } + + public bool? Participated { get; set; } + + public long? AchievedPoints { get; set; } +} \ No newline at end of file diff --git a/Application/Interfaces/ICustomEventParticipantRepository.cs b/Application/Interfaces/ICustomEventParticipantRepository.cs new file mode 100644 index 0000000..e467f9b --- /dev/null +++ b/Application/Interfaces/ICustomEventParticipantRepository.cs @@ -0,0 +1,17 @@ +using Application.Classes; +using Application.DataTransferObjects.CustomEventParticipant; + +namespace Application.Interfaces; + +public interface ICustomEventParticipantRepository +{ + Task> GetCustomEventParticipantAsync(Guid customEventParticipantId, + CancellationToken cancellationToken); + Task> InsertCustomEventParticipantAsync( + List createCustomEventParticipants, CancellationToken cancellationToken); + + Task>> GetPlayerCustomEventParticipantsAsync(Guid playerId, int last, CancellationToken cancellationToken); + + Task> UpdateCustomEventParticipantAsync( + UpdateCustomEventParticipantDto updateCustomEventParticipant, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Application/Profiles/CustomEventParticipantProfile.cs b/Application/Profiles/CustomEventParticipantProfile.cs index 15af4c6..9dc67f6 100644 --- a/Application/Profiles/CustomEventParticipantProfile.cs +++ b/Application/Profiles/CustomEventParticipantProfile.cs @@ -9,7 +9,11 @@ public class CustomEventParticipantProfile : Profile public CustomEventParticipantProfile() { CreateMap() - .ForMember(des => des.Id, opt => opt.MapFrom(src => Guid.CreateVersion7())) .ForMember(des => des.PlayerName, opt => opt.MapFrom(src => src.Player.PlayerName)); + + CreateMap() + .ForMember(des => des.Id, opt => opt.MapFrom(src => Guid.CreateVersion7())); + + CreateMap(); } } \ No newline at end of file diff --git a/Application/Profiles/CustomEventProfile.cs b/Application/Profiles/CustomEventProfile.cs index c2da4e9..68bdb1c 100644 --- a/Application/Profiles/CustomEventProfile.cs +++ b/Application/Profiles/CustomEventProfile.cs @@ -10,12 +10,15 @@ public class CustomEventProfile : Profile { CreateMap(); + CreateMap() + .ForMember(des => des.CustomEventParticipants, opt => opt.MapFrom(src => src.CustomEventParticipants)); + CreateMap() .ForMember(des => des.Id, opt => opt.MapFrom(src => Guid.CreateVersion7())) - .ForMember(des => des.EventDate, opt => opt.MapFrom(src => DateTime.Parse(src.EventDateString))); + .ForMember(des => des.EventDate, opt => opt.MapFrom(src => DateTime.Parse(src.EventDate))); CreateMap() .ForMember(des => des.ModifiedOn, opt => opt.MapFrom(src => DateTime.Now)) - .ForMember(des => des.EventDate, opt => opt.MapFrom(src => DateTime.Parse(src.EventDateString))); + .ForMember(des => des.EventDate, opt => opt.MapFrom(src => DateTime.Parse(src.EventDate))); } } \ No newline at end of file diff --git a/Application/Repositories/CustomEventParticipantRepository.cs b/Application/Repositories/CustomEventParticipantRepository.cs new file mode 100644 index 0000000..5f2a136 --- /dev/null +++ b/Application/Repositories/CustomEventParticipantRepository.cs @@ -0,0 +1,83 @@ +using Application.Classes; +using Application.DataTransferObjects.CustomEventParticipant; +using Application.Errors; +using Application.Interfaces; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Database; +using Database.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Application.Repositories; + +public class CustomEventParticipantRepository(ApplicationContext context, IMapper mapper, ILogger logger) : ICustomEventParticipantRepository +{ + public async Task> GetCustomEventParticipantAsync(Guid customEventParticipantId, CancellationToken cancellationToken) + { + var customEventParticipantById = await context.CustomEventParticipants + .ProjectTo(mapper.ConfigurationProvider) + .AsNoTracking() + .FirstOrDefaultAsync(customEventParticipant => customEventParticipant.Id == customEventParticipantId, + cancellationToken); + + return customEventParticipantById is null + ? Result.Failure(new Error("", "")) + : Result.Success(customEventParticipantById); + } + + public async Task> InsertCustomEventParticipantAsync(List createCustomEventParticipants, CancellationToken cancellationToken) + { + var customEventParticipants = mapper.Map>(createCustomEventParticipants); + + await context.CustomEventParticipants.AddRangeAsync(customEventParticipants, cancellationToken); + + try + { + await context.SaveChangesAsync(cancellationToken); + return Result.Success(true); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + return Result.Failure(GeneralErrors.DatabaseError); + } + } + + public async Task>> GetPlayerCustomEventParticipantsAsync(Guid playerId, int last, CancellationToken cancellationToken) + { + var customEventPlayerParticipated = await context.CustomEventParticipants + .Where(customEventParticipant => customEventParticipant.PlayerId == playerId) + .OrderByDescending(customEventParticipant => customEventParticipant.CustomEvent.EventDate) + .Take(last) + .ProjectTo(mapper.ConfigurationProvider) + .AsNoTracking() + .ToListAsync(cancellationToken); + + return Result.Success(customEventPlayerParticipated); + } + + public async Task> UpdateCustomEventParticipantAsync(UpdateCustomEventParticipantDto updateCustomEventParticipant, + CancellationToken cancellationToken) + { + var customEventParticipantToUpdate = await context.CustomEventParticipants + .FirstOrDefaultAsync( + customEventParticipant => customEventParticipant.Id == updateCustomEventParticipant.Id, + cancellationToken); + + if (customEventParticipantToUpdate is null) return Result.Failure(new Error("", "")); + + mapper.Map(updateCustomEventParticipant, customEventParticipantToUpdate); + + try + { + await context.SaveChangesAsync(cancellationToken); + return Result.Success(mapper.Map(customEventParticipantToUpdate)); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + return Result.Failure(GeneralErrors.DatabaseError); + } + } +} \ No newline at end of file diff --git a/Ui/src/app/app-routing.module.ts b/Ui/src/app/app-routing.module.ts index 50f1ac4..11d8e13 100644 --- a/Ui/src/app/app-routing.module.ts +++ b/Ui/src/app/app-routing.module.ts @@ -21,6 +21,7 @@ import {ResetPasswordComponent} from "./Authentication/reset-password/reset-pass import {CustomEventComponent} from "./pages/custom-event/custom-event.component"; import {ZombieSiegeComponent} from "./pages/zombie-siege/zombie-siege.component"; import {ZombieSiegeDetailComponent} from "./pages/zombie-siege/zombie-siege-detail/zombie-siege-detail.component"; +import {CustomEventDetailComponent} from "./pages/custom-event/custom-event-detail/custom-event-detail.component"; const routes: Routes = [ {path: 'players', component: PlayerComponent, canActivate: [authGuard]}, @@ -36,6 +37,7 @@ const routes: Routes = [ {path: 'account', component: AccountComponent, canActivate: [authGuard]}, {path: 'change-password', component: ChangePasswordComponent, canActivate: [authGuard]}, {path: 'custom-event', component: CustomEventComponent, canActivate: [authGuard]}, + {path: 'custom-event-detail/:id', component: CustomEventDetailComponent, canActivate: [authGuard]}, {path: 'zombie-siege', component: ZombieSiegeComponent, canActivate: [authGuard]}, {path: 'zombie-siege-detail/:id', component: ZombieSiegeDetailComponent, canActivate: [authGuard]}, {path: 'login', component: LoginComponent}, diff --git a/Ui/src/app/app.module.ts b/Ui/src/app/app.module.ts index a3a980d..2bb8d6c 100644 --- a/Ui/src/app/app.module.ts +++ b/Ui/src/app/app.module.ts @@ -50,6 +50,8 @@ import { UnderDevelopmentComponent } from './helpers/under-development/under-dev import { ZombieSiegeComponent } from './pages/zombie-siege/zombie-siege.component'; import { ZombieSiegeParticipantsModalComponent } from './modals/zombie-siege-participants-modal/zombie-siege-participants-modal.component'; import { ZombieSiegeDetailComponent } from './pages/zombie-siege/zombie-siege-detail/zombie-siege-detail.component'; +import { CustomEventParticipantsModelComponent } from './modals/custom-event-participants-model/custom-event-participants-model.component'; +import { CustomEventDetailComponent } from './pages/custom-event/custom-event-detail/custom-event-detail.component'; @NgModule({ declarations: [ @@ -90,7 +92,9 @@ import { ZombieSiegeDetailComponent } from './pages/zombie-siege/zombie-siege-de UnderDevelopmentComponent, ZombieSiegeComponent, ZombieSiegeParticipantsModalComponent, - ZombieSiegeDetailComponent + ZombieSiegeDetailComponent, + CustomEventParticipantsModelComponent, + CustomEventDetailComponent ], imports: [ BrowserModule, diff --git a/Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.css b/Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.css new file mode 100644 index 0000000..6cf1671 --- /dev/null +++ b/Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.css @@ -0,0 +1,9 @@ +.check-box-container { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.text-color { + color: #43c315; +} diff --git a/Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.html b/Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.html new file mode 100644 index 0000000..77df1d4 --- /dev/null +++ b/Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.html @@ -0,0 +1,68 @@ + + + + + +@if (participantsForm) { + +} + + + diff --git a/Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.ts b/Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.ts new file mode 100644 index 0000000..f23d42e --- /dev/null +++ b/Ui/src/app/modals/custom-event-participants-model/custom-event-participants-model.component.ts @@ -0,0 +1,75 @@ +import {Component, inject, Input, OnInit} from '@angular/core'; +import {PlayerService} from "../../services/player.service"; +import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; +import {PlayerModel} from "../../models/player.model"; +import {FormArray, FormControl, FormGroup, Validators} from "@angular/forms"; + +@Component({ + selector: 'app-custom-event-participants-model', + templateUrl: './custom-event-participants-model.component.html', + styleUrl: './custom-event-participants-model.component.css' +}) +export class CustomEventParticipantsModelComponent implements OnInit { + + private readonly _playerService: PlayerService = inject(PlayerService); + + public activeModal: NgbActiveModal = inject(NgbActiveModal); + + public playerParticipated: {playerId: string, playerName: string, participated: boolean, achievedPoints: number}[] = []; + public participantsForm!: FormGroup; + public isUpdate: boolean = false; + + @Input({required: true}) allianceId!: string; + @Input({required: true}) isPointEvent!: boolean; + @Input({required: true}) isParticipationEvent!: boolean; + @Input() players: {playerId: string, playerName: string, participated: boolean, achievedPoints: number}[] | undefined; + + get customEventParticipants(): FormArray { + return this.participantsForm.get('customEventParticipants') as FormArray; + } + + + ngOnInit() { + if (this.players) { + this.isUpdate = true; + this.playerParticipated = [...this.players]; + this.createParticipantForm(); + } else { + this.getPlayers(); + } + } + + getPlayers() { + this._playerService.getAlliancePlayer(this.allianceId).subscribe({ + next: ((response) => { + if (response) { + response.forEach((player: PlayerModel) => { + this.playerParticipated.push({playerId: player.id, playerName: player.playerName, participated: false, achievedPoints: 0}); + }); + this.playerParticipated.sort((a, b) => a.playerName.localeCompare(b.playerName)); + } + this.createParticipantForm(); + }) + }); + } + + createParticipantForm() { + this.participantsForm = new FormGroup({ + customEventParticipants: new FormArray([]) + }); + this.playerParticipated.forEach(player => { + this.customEventParticipants.push(new FormGroup({ + id: new FormControl(''), + playerId: new FormControl(player.playerId), + playerName: new FormControl(player.playerName), + participated: new FormControl(player.participated), + achievedPoints: new FormControl(player.achievedPoints, [Validators.required, Validators.pattern('(0|[1-9]\\d*)')]) + })) + }); + } + + onSubmit() { + const participants = this.participantsForm.value.customEventParticipants; + this.activeModal.close(participants); + } +} diff --git a/Ui/src/app/models/customEvent.model.ts b/Ui/src/app/models/customEvent.model.ts index 7cd5a27..0c189eb 100644 --- a/Ui/src/app/models/customEvent.model.ts +++ b/Ui/src/app/models/customEvent.model.ts @@ -5,12 +5,25 @@ export interface CustomEventModel { allianceId: string; name: string; description: string; - isPointsEvent?: boolean; - isParticipationEvent?: boolean; + isPointsEvent: boolean; + isParticipationEvent: boolean; eventDate: Date; isInProgress: boolean; + createdBy: string; + modifiedBy?: string; + modifiedOn?: Date; } export interface CustomEventDetailModel extends CustomEventModel { customEventParticipants: CustomEventParticipantModel[]; } + +export interface CreateCustomEventModel { + name: string; + description: string; + isPointsEvent: boolean; + isParticipationEvent: boolean; + eventDate: string; + allianceId: string; + isInProgress: boolean; +} diff --git a/Ui/src/app/models/customEventParticipant.model.ts b/Ui/src/app/models/customEventParticipant.model.ts index c0c3b41..58cb684 100644 --- a/Ui/src/app/models/customEventParticipant.model.ts +++ b/Ui/src/app/models/customEventParticipant.model.ts @@ -2,7 +2,7 @@ export interface CustomEventParticipantModel { id: string; playerId: string; customEventId: string; - participated?: boolean; - achievedPoints?: number; + participated: boolean; + achievedPoints: number; playerName: string; } diff --git a/Ui/src/app/pages/custom-event/custom-event-detail/custom-event-detail.component.css b/Ui/src/app/pages/custom-event/custom-event-detail/custom-event-detail.component.css new file mode 100644 index 0000000..e69de29 diff --git a/Ui/src/app/pages/custom-event/custom-event-detail/custom-event-detail.component.html b/Ui/src/app/pages/custom-event/custom-event-detail/custom-event-detail.component.html new file mode 100644 index 0000000..2f97aba --- /dev/null +++ b/Ui/src/app/pages/custom-event/custom-event-detail/custom-event-detail.component.html @@ -0,0 +1,67 @@ +
+ +
+ +
+ + @if (customEventDetail) { +
+
+
{{customEventDetail.eventDate | date: 'dd.MM.yyyy'}}
+
+
+
Name: {{customEventDetail.name}}
+

Description: {{customEventDetail.description}}

+

Point event: + +

+

Participation event: + +

+ @if (customEventDetail.isParticipationEvent) { +

Participation rate:

+ + } +

Status: + @if (customEventDetail.isInProgress) { + In Progress + } @else { + Done + } +

+
+
+

Creator: {{customEventDetail.createdBy}}

+ @if (customEventDetail.modifiedOn) { +

Modified: {{customEventDetail.modifiedOn | date: 'dd.MM.yyyy HH:mm'}} + by {{customEventDetail.modifiedBy}}

+ } +
+
+

Participants

+
+
+ @for (player of customEventDetail.customEventParticipants; track player.id) { + +
+
+
{{player.playerName}}
+ @if (customEventDetail.isPointsEvent) { +
+ Achieved Points: {{player.achievedPoints}} +
+ } + @if (customEventDetail.isParticipationEvent) { +
+ Participated: +
+ } +
+
+ } +
+
+
+
+ } +
diff --git a/Ui/src/app/pages/custom-event/custom-event-detail/custom-event-detail.component.ts b/Ui/src/app/pages/custom-event/custom-event-detail/custom-event-detail.component.ts new file mode 100644 index 0000000..02e8d98 --- /dev/null +++ b/Ui/src/app/pages/custom-event/custom-event-detail/custom-event-detail.component.ts @@ -0,0 +1,48 @@ +import {Component, inject, OnInit} from '@angular/core'; +import {ActivatedRoute} from "@angular/router"; +import {CustomEventService} from "../../../services/custom-event.service"; +import {CustomEventDetailModel} from "../../../models/customEvent.model"; + +@Component({ + selector: 'app-custom-event-detail', + templateUrl: './custom-event-detail.component.html', + styleUrl: './custom-event-detail.component.css' +}) +export class CustomEventDetailComponent implements OnInit { + + private readonly _activatedRote: ActivatedRoute = inject(ActivatedRoute); + private readonly _customEventService: CustomEventService = inject(CustomEventService); + + private customEventId!: string; + + public customEventDetail: CustomEventDetailModel | undefined; + public participatedPlayers: number = 0; + + ngOnInit() { + this.customEventId = this._activatedRote.snapshot.params['id']; + + if (!this.customEventId) { + // TODO + } + this.getCustomEventDetail(this.customEventId); + } + + getCustomEventDetail(customEventId: string) { + this._customEventService.getCustomEventDetail(customEventId).subscribe({ + next: ((response: CustomEventDetailModel) => { + if (response) { + this.participatedPlayers = 0; + this.customEventDetail = response; + if (this.customEventDetail.isParticipationEvent) { + this.customEventDetail.customEventParticipants.forEach((player) => { + if (player.participated) { + this.participatedPlayers++; + } + }) + } + } + }) + }); + } + +} diff --git a/Ui/src/app/pages/custom-event/custom-event.component.html b/Ui/src/app/pages/custom-event/custom-event.component.html index caaccea..2cb2d57 100644 --- a/Ui/src/app/pages/custom-event/custom-event.component.html +++ b/Ui/src/app/pages/custom-event/custom-event.component.html @@ -1,6 +1,155 @@

Custom Event

- + @if (!isCreateCustomEvent) { +
+ +
+ } + + @if (isCreateCustomEvent) { +
+ +
+ + + @if (f['eventDate'].invalid && (f['eventDate'].dirty || f['eventDate'].touched)) { +
+ @if (f['eventDate'].hasError('required')) { +

eventDate is required

+ } +
+ } +
+ +
+ + + @if (f['name'].invalid && (f['name'].dirty || f['name'].touched)) { +
+ @if (f['name'].hasError('required')) { +

Name is required

+ } + @if (f['name'].hasError('maxlength')) { +

Maximum 150 characters allowed

+ } +
+ } +
+ +
+ + + @if (f['description'].invalid && (f['description'].touched || f['description'].dirty)) { +
+ @if (f['description'].hasError('required')) { +

Description is required

+ } + @if (f['description'].hasError('maxlength')) { +

Maximum 500 characters allowed

+ } +
+ } +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + @if (!playerSelected) { +
+

Please add participants

+
+ } + +
+ +
+
+ + +
+
+ } + + @if (!isCreateCustomEvent) { + @if (customEvents.length > 0) { +
+ + + + + + + + + + + + + + @for (customEvent of customEvents; track customEvent.id) { + + + + + + + + + + } + +
Event DateNamePoint eventParticipation eventStatusCreatorAction
{{customEvent.eventDate | date: 'dd.MM.yyyy'}}{{customEvent.name}} + + + + + @if (customEvent.isInProgress) { + In Progress + } @else { + Done + } + {{customEvent.createdBy}} +
+ + + +
+
+
+ } @else { + + } + }
diff --git a/Ui/src/app/pages/custom-event/custom-event.component.ts b/Ui/src/app/pages/custom-event/custom-event.component.ts index 5052a4c..5a1327f 100644 --- a/Ui/src/app/pages/custom-event/custom-event.component.ts +++ b/Ui/src/app/pages/custom-event/custom-event.component.ts @@ -1,10 +1,257 @@ -import { Component } from '@angular/core'; +import {Component, inject, OnInit} from '@angular/core'; +import {FormControl, FormGroup, Validators} from "@angular/forms"; +import {JwtTokenService} from "../../services/jwt-token.service"; +import {CreateCustomEventModel, CustomEventDetailModel, CustomEventModel} from "../../models/customEvent.model"; +import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; +import { + CustomEventParticipantsModelComponent +} from "../../modals/custom-event-participants-model/custom-event-participants-model.component"; +import {CustomEventService} from "../../services/custom-event.service"; +import {CustomEventParticipantModel} from "../../models/customEventParticipant.model"; +import {CustomEventParticipantService} from "../../services/custom-event-participant.service"; +import {Router} from "@angular/router"; +import Swal from "sweetalert2"; +import {ToastrService} from "ngx-toastr"; +import {forkJoin, Observable} from "rxjs"; @Component({ selector: 'app-custom-event', templateUrl: './custom-event.component.html', styleUrl: './custom-event.component.css' }) -export class CustomEventComponent { +export class CustomEventComponent implements OnInit { + customEvents: CustomEventModel[] = []; + customEventDetail!: CustomEventDetailModel; + isCreateCustomEvent: boolean = false; + customEventForm!: FormGroup; + playerSelected: boolean = false; + playerParticipated: { playerId: string, playerName: string, participated: boolean, achievedPoints: number }[] = []; + customEventParticipants: CustomEventParticipantModel[] = []; + isUpdate: boolean = false; + private readonly _tokenService: JwtTokenService = inject(JwtTokenService); + private readonly _modalService: NgbModal = inject(NgbModal); + private readonly _customEventService: CustomEventService = inject(CustomEventService); + private readonly _customEventParticipantService: CustomEventParticipantService = inject(CustomEventParticipantService); + private readonly _router: Router = inject(Router); + private readonly _toastr: ToastrService = inject(ToastrService); + private allianceId: string = this._tokenService.getAllianceId()!; + + get f() { + return this.customEventForm.controls; + } + + ngOnInit() { + this.getCustomEvents(10); + } + + getCustomEvents(take: number) { + this.customEvents = []; + this._customEventService.getAllianceCustomEvents(this.allianceId, take).subscribe({ + next: ((response: CustomEventModel[]) => { + if (response) { + this.customEvents = response; + } + }), + error: (error) => { + console.error(error); + } + }); + } + + + onCreateEvent() { + this.createCustomEventForm(false); + } + + + createCustomEventForm(isUpdate: boolean, customEventDetail: CustomEventDetailModel | null = null) { + if (isUpdate) { + this.playerSelected = true; + this.playerParticipated = [...customEventDetail!.customEventParticipants]; + } else { + this.playerSelected = false; + } + const d = isUpdate ? new Date(customEventDetail!.eventDate) : new Date(); + this.customEventForm = new FormGroup({ + id: new FormControl(isUpdate ? customEventDetail!.id : ''), + allianceId: new FormControl(this.allianceId), + name: new FormControl(isUpdate ? customEventDetail!.name : '', [Validators.required, Validators.maxLength(150)]), + description: new FormControl(isUpdate ? customEventDetail!.description : '', [Validators.required, Validators.maxLength(500)]), + isPointsEvent: new FormControl(isUpdate ? customEventDetail!.isPointsEvent : false), + isParticipationEvent: new FormControl(isUpdate ? customEventDetail!.isParticipationEvent : false), + eventDate: new FormControl(new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())).toISOString().substring(0, 10)), + isInProgress: new FormControl(isUpdate ? customEventDetail!.isInProgress : true) + }); + this.isCreateCustomEvent = true; + } + + onAddParticipants() { + const modalRef = this._modalService.open(CustomEventParticipantsModelComponent, + {animation: true, backdrop: 'static', centered: true, size: 'lg', scrollable: true}); + if (this.playerSelected) { + this.playerParticipated.sort((a, b) => a.playerName.localeCompare(b.playerName)); + modalRef.componentInstance.players = [...this.playerParticipated]; + } + modalRef.componentInstance.allianceId = this.allianceId; + modalRef.componentInstance.isParticipationEvent = this.customEventForm.controls['isParticipationEvent'].value; + modalRef.componentInstance.isPointEvent = this.customEventForm.controls['isPointsEvent'].value; + modalRef.closed.subscribe({ + next: ((response: any[]) => { + if (response) { + this.playerSelected = true; + this.playerParticipated = [...response]; + this.playerParticipated.sort((a, b) => a.playerName.localeCompare(b.playerName)); + } + }) + }) + } + + onCancel() { + this.isUpdate = false; + this.isCreateCustomEvent = false; + } + + onSubmit() { + if (this.customEventForm.invalid) { + return; + } + if (this.isUpdate) { + this.updateCustomEvent(); + return; + } + const customEvent: CreateCustomEventModel = this.customEventForm.value as CreateCustomEventModel; + this._customEventService.createCustomEvent(customEvent).subscribe({ + next: ((response) => { + if (response) { + this.insertCustomEventParticipants(response.id); + } + }), + error: ((error: any) => { + console.error(error); + }) + }) + } + + insertCustomEventParticipants(customEventId: string) { + const customEventParticipants: CustomEventParticipantModel[] = []; + this.playerParticipated.forEach((player) => { + const participant: CustomEventParticipantModel = { + id: '', + customEventId: customEventId, + participated: player.participated, + playerId: player.playerId, + achievedPoints: player.achievedPoints, + playerName: player.playerName + }; + customEventParticipants.push(participant); + }); + this._customEventParticipantService.insertCustomEventParticipants(customEventParticipants).subscribe({ + next: (() => { + this.playerParticipated = []; + this.onCancel(); + this.getCustomEvents(10); + }), + error: (error) => { + console.log(error); + } + }); + } + + onGoToCustomEventDetail(customEvent: CustomEventModel) { + this._router.navigate(['custom-event-detail', customEvent.id]).then(); + } + + onEditCustomEvent(customEvent: CustomEventModel) { + this._customEventService.getCustomEventDetail(customEvent.id).subscribe({ + next: ((response) => { + if (response) { + this.customEventParticipants = structuredClone(response.customEventParticipants); + this.isUpdate = true; + this.createCustomEventForm(true, response); + + this.customEventDetail = response; + } + }) + }); + } + + onDeleteCustomEvent(customEvent: CustomEventModel) { + Swal.fire({ + title: "Delete Custom event ?", + text: `Do you really want to delete the custom event`, + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "Yes, delete it!" + }).then((result) => { + if (result.isConfirmed) { + this._customEventService.deleteCustomEvent(customEvent.id).subscribe({ + next: ((response) => { + if (response) { + Swal.fire({ + title: "Deleted!", + text: "Custom event has been deleted", + icon: "success" + }).then(_ => this.getCustomEvents(10)); + } + }), + error: (error: Error) => { + console.log(error); + } + }); + } + }); + } + + private updateCustomEvent() { + const participantsToUpdate: any[] = []; + const customEvent: CustomEventModel = this.customEventForm.value as CustomEventModel; + this.customEventParticipants.forEach((participant) => { + const player = this.playerParticipated.find(p => p.playerId === participant.playerId); + if (player!.participated !== participant.participated || player!.achievedPoints !== participant.achievedPoints) { + const playerToUpdate: CustomEventParticipantModel = { + playerId: player!.playerId, + achievedPoints: player!.achievedPoints, + playerName: player!.playerName, + participated: player!.participated, + id: participant.id, + customEventId: participant.customEventId, + } + participantsToUpdate.push(playerToUpdate); + } + }); + this._customEventService.updateCustomEvent(customEvent.id, customEvent).subscribe({ + next: ((response) => { + if (response) { + this.updateCustomEventParticipants(participantsToUpdate); + } + }) + }); + } + + private updateCustomEventParticipants(participantsToUpdate: any[]) { + if (participantsToUpdate.length <= 0) { + this._toastr.success('Successfully updated!', 'Successfully'); + this.onCancel(); + this.getCustomEvents(10); + return; + } + + const requests: Observable[] = []; + participantsToUpdate.forEach(participant => { + const request = this._customEventParticipantService.updateCustomEventParticipant(participant.id, participant); + requests.push(request); + }) + forkJoin(requests).subscribe({ + next: ((response) => { + if (response) { + this._toastr.success('Successfully updated!', 'Successfully'); + this.onCancel(); + this.getCustomEvents(10); + } + }) + }) + } } diff --git a/Ui/src/app/services/custom-event-participant.service.ts b/Ui/src/app/services/custom-event-participant.service.ts new file mode 100644 index 0000000..0111ae7 --- /dev/null +++ b/Ui/src/app/services/custom-event-participant.service.ts @@ -0,0 +1,23 @@ +import {inject, Injectable} from '@angular/core'; +import {environment} from "../../environments/environment"; +import {HttpClient} from "@angular/common/http"; +import {CustomEventParticipantModel} from "../models/customEventParticipant.model"; +import {Observable} from "rxjs"; + +@Injectable({ + providedIn: 'root' +}) +export class CustomEventParticipantService { + + private readonly _serviceUrl = environment.apiBaseUrl + 'customEventParticipants/'; + private readonly _httpClient: HttpClient = inject(HttpClient); + + insertCustomEventParticipants(customEventParticipants: CustomEventParticipantModel[]): Observable { + return this._httpClient.post(this._serviceUrl, customEventParticipants); + } + + updateCustomEventParticipant(customEventParticipantId: string, customEventParticipantModel: CustomEventParticipantModel): Observable { + return this._httpClient.put(this._serviceUrl + customEventParticipantId, customEventParticipantModel); + } + +} diff --git a/Ui/src/app/services/custom-event.service.ts b/Ui/src/app/services/custom-event.service.ts new file mode 100644 index 0000000..83d2d2d --- /dev/null +++ b/Ui/src/app/services/custom-event.service.ts @@ -0,0 +1,38 @@ +import {inject, Injectable} from '@angular/core'; +import {environment} from "../../environments/environment"; +import {HttpClient, HttpParams} from "@angular/common/http"; +import {Observable} from "rxjs"; +import {CreateCustomEventModel, CustomEventDetailModel, CustomEventModel} from "../models/customEvent.model"; + +@Injectable({ + providedIn: 'root' +}) +export class CustomEventService { + + private readonly _serviceUrl = environment.apiBaseUrl + 'customEvents/'; + private readonly _httpClient: HttpClient = inject(HttpClient); + + + getAllianceCustomEvents(allianceId: string, take: number): Observable { + let params = new HttpParams(); + params = params.append('take', take); + return this._httpClient.get(this._serviceUrl + 'Alliance/' + allianceId, {params: params}); + } + + getCustomEventDetail(customEventId: string): Observable { + return this._httpClient.get(this._serviceUrl + 'GetCustomEventDetail/' + customEventId); + } + + createCustomEvent(customEvent: CreateCustomEventModel): Observable { + return this._httpClient.post(this._serviceUrl, customEvent); + } + + updateCustomEvent(customEventId: string, customEvent: CustomEventModel): Observable { + return this._httpClient.put(this._serviceUrl + customEventId, customEvent); + } + + deleteCustomEvent(customEventId: string): Observable { + return this._httpClient.delete(this._serviceUrl + customEventId); + } + +}