Implement custom event

This commit is contained in:
Tomasi - Developing 2024-12-17 11:27:47 +01:00
parent 4ac162e8dc
commit bb735a68aa
25 changed files with 1010 additions and 14 deletions

View File

@ -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<CustomEventParticipantsController> logger) : ControllerBase
{
[HttpGet("{customEventParticipantId:guid}")]
public async Task<ActionResult<CustomEventParticipantDto>> 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<ActionResult<List<CreateCustomEventParticipantDto>>> 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<IActionResult> InsertCustomEventParticipant(
List<CreateCustomEventParticipantDto> 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<ActionResult<CustomEventParticipantDto>> 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);
}
}
}
}

View File

@ -10,7 +10,6 @@
<Folder Include="DataTransferObjects\Alliance\" />
<Folder Include="Classes\" />
<Folder Include="DataTransferObjects\MarshalGuardParticipant\" />
<Folder Include="DataTransferObjects\CustomEventParticipant\" />
<Folder Include="DataTransferObjects\ZombieSiegeParticipant\" />
<Folder Include="DataTransferObjects\Rank\" />
<Folder Include="DataTransferObjects\Note\" />

View File

@ -24,6 +24,7 @@ public static class ApplicationDependencyInjection
services.AddScoped<IAuthenticationRepository, AuthenticationRepository>();
services.AddScoped<IRankRepository, RankRepository>();
services.AddScoped<ICustomEventRepository, CustomEventRepository>();
services.AddScoped<ICustomEventParticipantRepository, CustomEventParticipantRepository>();
services.AddScoped<IMarshalGuardParticipantRepository, MarshalGuardParticipantRepository>();
services.AddScoped<IVsDuelParticipantRepository, VsDuelParticipantRepository>();
services.AddScoped<IUserRepository, UserRepository>();

View File

@ -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; }
}

View File

@ -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; }

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -0,0 +1,17 @@
using Application.Classes;
using Application.DataTransferObjects.CustomEventParticipant;
namespace Application.Interfaces;
public interface ICustomEventParticipantRepository
{
Task<Result<CustomEventParticipantDto>> GetCustomEventParticipantAsync(Guid customEventParticipantId,
CancellationToken cancellationToken);
Task<Result<bool>> InsertCustomEventParticipantAsync(
List<CreateCustomEventParticipantDto> createCustomEventParticipants, CancellationToken cancellationToken);
Task<Result<List<CustomEventParticipantDto>>> GetPlayerCustomEventParticipantsAsync(Guid playerId, int last, CancellationToken cancellationToken);
Task<Result<CustomEventParticipantDto>> UpdateCustomEventParticipantAsync(
UpdateCustomEventParticipantDto updateCustomEventParticipant, CancellationToken cancellationToken);
}

View File

@ -9,7 +9,11 @@ public class CustomEventParticipantProfile : Profile
public CustomEventParticipantProfile()
{
CreateMap<CustomEventParticipant, CustomEventParticipantDto>()
.ForMember(des => des.Id, opt => opt.MapFrom(src => Guid.CreateVersion7()))
.ForMember(des => des.PlayerName, opt => opt.MapFrom(src => src.Player.PlayerName));
CreateMap<CreateCustomEventParticipantDto, CustomEventParticipant>()
.ForMember(des => des.Id, opt => opt.MapFrom(src => Guid.CreateVersion7()));
CreateMap<UpdateCustomEventParticipantDto, CustomEventParticipant>();
}
}

View File

@ -10,12 +10,15 @@ public class CustomEventProfile : Profile
{
CreateMap<CustomEvent, CustomEventDto>();
CreateMap<CustomEvent, CustomEventDetailDto>()
.ForMember(des => des.CustomEventParticipants, opt => opt.MapFrom(src => src.CustomEventParticipants));
CreateMap<CreateCustomEventDto, CustomEvent>()
.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<UpdateCustomEventDto, CustomEvent>()
.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)));
}
}

View File

@ -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<CustomEventParticipantRepository> logger) : ICustomEventParticipantRepository
{
public async Task<Result<CustomEventParticipantDto>> GetCustomEventParticipantAsync(Guid customEventParticipantId, CancellationToken cancellationToken)
{
var customEventParticipantById = await context.CustomEventParticipants
.ProjectTo<CustomEventParticipantDto>(mapper.ConfigurationProvider)
.AsNoTracking()
.FirstOrDefaultAsync(customEventParticipant => customEventParticipant.Id == customEventParticipantId,
cancellationToken);
return customEventParticipantById is null
? Result.Failure<CustomEventParticipantDto>(new Error("", ""))
: Result.Success(customEventParticipantById);
}
public async Task<Result<bool>> InsertCustomEventParticipantAsync(List<CreateCustomEventParticipantDto> createCustomEventParticipants, CancellationToken cancellationToken)
{
var customEventParticipants = mapper.Map<List<CustomEventParticipant>>(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<bool>(GeneralErrors.DatabaseError);
}
}
public async Task<Result<List<CustomEventParticipantDto>>> 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<CustomEventParticipantDto>(mapper.ConfigurationProvider)
.AsNoTracking()
.ToListAsync(cancellationToken);
return Result.Success(customEventPlayerParticipated);
}
public async Task<Result<CustomEventParticipantDto>> UpdateCustomEventParticipantAsync(UpdateCustomEventParticipantDto updateCustomEventParticipant,
CancellationToken cancellationToken)
{
var customEventParticipantToUpdate = await context.CustomEventParticipants
.FirstOrDefaultAsync(
customEventParticipant => customEventParticipant.Id == updateCustomEventParticipant.Id,
cancellationToken);
if (customEventParticipantToUpdate is null) return Result.Failure<CustomEventParticipantDto>(new Error("", ""));
mapper.Map(updateCustomEventParticipant, customEventParticipantToUpdate);
try
{
await context.SaveChangesAsync(cancellationToken);
return Result.Success(mapper.Map<CustomEventParticipantDto>(customEventParticipantToUpdate));
}
catch (Exception e)
{
logger.LogError(e, e.Message);
return Result.Failure<CustomEventParticipantDto>(GeneralErrors.DatabaseError);
}
}
}

View File

@ -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},

View File

@ -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,

View File

@ -0,0 +1,9 @@
.check-box-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.text-color {
color: #43c315;
}

View File

@ -0,0 +1,68 @@
<div class="modal-header flex-column" xmlns="http://www.w3.org/1999/html">
<div class="d-flex justify-content-between align-items-center w-100">
<h4 class="modal-title">Select Player for Custom event</h4>
<button (click)="activeModal.dismiss()" aria-label="Close" class="btn-close" type="button"></button>
</div>
</div>
<div class="modal-body">
<div class="row">
@if (!isParticipationEvent && !isPointEvent) {
<p class="text-danger">Please choose an event type like points, participation or both</p>
} @else {
@if (participantsForm) {
<form [formGroup]="participantsForm" class="row g-3">
<ng-container formArrayName="customEventParticipants">
@for (participant of customEventParticipants.controls; track participant; let i=$index) {
<ng-container [formGroupName]="i">
<div class="col-12 col-md-6 mb-4">
<div class="d-flex flex-column border p-3">
<h6 class="mb-2 text-color">{{participant.get('playerName')?.value}}</h6>
@if (isPointEvent) {
<div class="col mb-2">
<label for="point{{i}}">Achieved Points</label>
<input [ngClass]="{
'is-valid': participant.get('achievedPoints')?.valid,
'is-invalid': participant.get('achievedPoints')?.invalid && (participant.get('achievedPoints')?.touched || participant.get('achievedPoints')?.dirty)
}" type="number" min="0" class="form-control form-control-sm" id="point{{i}}" formControlName="achievedPoints">
@if (participant.get('achievedPoints')?.invalid && (participant.get('achievedPoints')?.touched || participant.get('achievedPoints')?.dirty)) {
<div class="invalid-feedback">
@if (participant.get('achievedPoints')?.hasError('required')){
<p>points is required</p>
}
@if (participant.get('achievedPoints')?.hasError('pattern')){
<p>points must not be less than 0</p>
}
</div>
}
</div>
}
@if (isParticipationEvent) {
<div class="col">
<input class="form-check-input" id="participated{{i}}" formControlName="participated" type="checkbox">
<label class="form-check-label ps-2" for="participated{{i}}"> Participated</label>
</div>
}
</div>
</div>
</ng-container>
}
</ng-container>
</form>
}
}
</div>
</div>
@if (participantsForm) {
<div class="modal-footer">
<button (click)="activeModal.dismiss()" class="btn btn-warning" type="button">Close</button>
<button (click)="onSubmit()" [disabled]="!isPointEvent && !isParticipationEvent || participantsForm.invalid"
class=" btn btn-success" type="submit">{{isUpdate ? 'Update Participants' : 'Add to event'}}
</button>
</div>
}

View File

@ -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<string>(''),
playerId: new FormControl(player.playerId),
playerName: new FormControl<string>(player.playerName),
participated: new FormControl<boolean>(player.participated),
achievedPoints: new FormControl<number>(player.achievedPoints, [Validators.required, Validators.pattern('(0|[1-9]\\d*)')])
}))
});
}
onSubmit() {
const participants = this.participantsForm.value.customEventParticipants;
this.activeModal.close(participants);
}
}

View File

@ -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;
}

View File

@ -2,7 +2,7 @@ export interface CustomEventParticipantModel {
id: string;
playerId: string;
customEventId: string;
participated?: boolean;
achievedPoints?: number;
participated: boolean;
achievedPoints: number;
playerName: string;
}

View File

@ -0,0 +1,67 @@
<div class="container mt-3 pb-5">
<!-- Back button-->
<div class="d-grid gap-2 col-6 mx-auto">
<button routerLink="/custom-event" class="btn btn-primary" type="button"><i class="bi bi-arrow-left"></i> Back</button>
</div>
@if (customEventDetail) {
<div class="card mt-5">
<h5 class="card-header text-center">
<div>{{customEventDetail.eventDate | date: 'dd.MM.yyyy'}}</div>
</h5>
<div class="card-body">
<h5 class="card-title">Name: <span class="text-primary">{{customEventDetail.name}}</span></h5>
<p class="card-text">Description: <span class="text-primary">{{customEventDetail.description}}</span></p>
<p class="card-text">Point event:
<i class="bi " [ngClass]="customEventDetail.isPointsEvent ? 'bi-check-lg text-success' : 'bi-x-lg text-danger'"></i>
</p>
<p class="card-text">Participation event:
<i class="bi " [ngClass]="customEventDetail.isParticipationEvent ? 'bi-check-lg text-success' : 'bi-x-lg text-danger'"></i>
</p>
@if (customEventDetail.isParticipationEvent) {
<p class="card-text">Participation rate: <ngb-progressbar class="mb-3" type="success" [value]="participatedPlayers" [max]="customEventDetail.customEventParticipants.length" [showValue]="true" /></p>
}
<p class="card-text">Status:
@if (customEventDetail.isInProgress) {
<span class="text-primary"> In Progress</span>
} @else {
<span class="text-primary"> Done</span>
}
</p>
<hr>
<div>
<p class="card-text">Creator: <span class="text-primary">{{customEventDetail.createdBy}}</span></p>
@if (customEventDetail.modifiedOn) {
<p class="card-text">Modified: <span class="text-primary">{{customEventDetail.modifiedOn | date: 'dd.MM.yyyy HH:mm'}}</span>
by <span class="text-primary">{{customEventDetail.modifiedBy}}</span></p>
}
</div>
<hr>
<p class="card-text text-center">Participants</p>
<div ngbScrollSpy class="bg-light p-3 rounded-2 mb-3" style="height: 200px">
<div class="row">
@for (player of customEventDetail.customEventParticipants; track player.id) {
<div class="col-12 col-md-6 mb-4">
<div class="d-flex flex-column border p-3">
<h6 class="mb-1 text-success">{{player.playerName}}</h6>
@if (customEventDetail.isPointsEvent) {
<div class="col mb-1">
Achieved Points: <span class="text-primary">{{player.achievedPoints}}</span>
</div>
}
@if (customEventDetail.isParticipationEvent) {
<div class="col">
Participated: <i class="bi " [ngClass]="player.participated ? 'bi-check-lg text-success' : 'bi-x-lg text-danger'"></i>
</div>
}
</div>
</div>
}
</div>
</div>
</div>
</div>
}
</div>

View File

@ -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++;
}
})
}
}
})
});
}
}

View File

@ -1,6 +1,155 @@
<div class="container mt-3 pb-5">
<h2 class="text-center">Custom Event</h2>
<app-under-development></app-under-development>
@if (!isCreateCustomEvent) {
<div class="d-grid gap-2 col-6 mx-auto">
<button (click)="onCreateEvent()" class="btn btn-primary" type="button">Create new Event</button>
</div>
}
@if (isCreateCustomEvent) {
<form [formGroup]="customEventForm">
<!-- Event date-->
<div class="form-floating mb-3 is-invalid">
<input [ngClass]="{
'is-invalid': f['eventDate'].invalid && (f['eventDate'].dirty || f['eventDate'].touched),
'is-valid': f['eventDate'].valid}"
type="date" class="form-control" id="eventDate" placeholder="eventDate" formControlName="eventDate">
<label for="eventDate">Event date</label>
@if (f['eventDate'].invalid && (f['eventDate'].dirty || f['eventDate'].touched)) {
<div class="invalid-feedback">
@if (f['eventDate'].hasError('required')) {
<p>eventDate is required</p>
}
</div>
}
</div>
<!-- Event name-->
<div class="form-floating mb-3 is-invalid">
<input [ngClass]="{
'is-invalid': f['name'].invalid && (f['name'].dirty || f['name'].touched),
'is-valid': f['name'].valid}"
type="text" class="form-control" maxlength="151" id="name" placeholder="name" formControlName="name">
<label for="name">Event name</label>
@if (f['name'].invalid && (f['name'].dirty || f['name'].touched)) {
<div class="invalid-feedback">
@if (f['name'].hasError('required')) {
<p>Name is required</p>
}
@if (f['name'].hasError('maxlength')) {
<p>Maximum 150 characters allowed</p>
}
</div>
}
</div>
<!-- Description-->
<div class="form-floating mb-3">
<textarea [ngClass]="{
'is-valid': f['description'].valid,
'is-invalid': f['description'].invalid && (f['description'].touched || f['description'].dirty)
}" class="form-control" maxlength="501" formControlName="description" placeholder="description" id="description" style="height: 100px"></textarea>
<label for="description">description</label>
@if (f['description'].invalid && (f['description'].touched || f['description'].dirty)) {
<div class="invalid-feedback">
@if (f['description'].hasError('required')) {
<p>Description is required</p>
}
@if (f['description'].hasError('maxlength')) {
<p>Maximum 500 characters allowed</p>
}
</div>
}
</div>
<!-- In progress-->
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" formControlName="isInProgress" id="isInProgress">
<label class="form-check-label" for="isInProgress">
In Progress
</label>
</div>
<!-- Is point event-->
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" formControlName="isPointsEvent" id="isPointsEvent">
<label class="form-check-label" for="isPointsEvent">
Points Event
</label>
</div>
<!-- Is participation event-->
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" formControlName="isParticipationEvent" id="isParticipationEvent">
<label class="form-check-label" for="isParticipationEvent">
Participation Event
</label>
</div>
@if (!playerSelected) {
<div class="d-flex justify-content-center">
<p class="text-warning">Please add participants</p>
</div>
}
<div class="d-grid gap-2 col-6 mx-auto">
<button (click)="onAddParticipants()" class="btn btn-primary" type="button">{{isUpdate ? 'Update Participants' : 'Add Participants'}}</button>
</div>
<div class="d-flex justify-content-between">
<button (click)="onCancel()" type="button" class="btn btn-warning">Cancel</button>
<button [disabled]="customEventForm.invalid || playerParticipated.length <= 0" (click)="onSubmit()" type="submit" class="btn btn-success">{{isUpdate ? 'Update': 'Create'}}</button>
</div>
</form>
}
@if (!isCreateCustomEvent) {
@if (customEvents.length > 0) {
<div class="table-responsive mt-5">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th scope="col">Event Date</th>
<th scope="col">Name</th>
<th scope="col">Point event</th>
<th scope="col">Participation event</th>
<th scope="col">Status</th>
<th scope="col">Creator</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
@for (customEvent of customEvents; track customEvent.id) {
<tr>
<td>{{customEvent.eventDate | date: 'dd.MM.yyyy'}}</td>
<td>{{customEvent.name}}</td>
<td>
<i class="bi " [ngClass]="customEvent.isPointsEvent ? 'bi-check-lg text-success' : 'bi-x-lg text-danger'"></i>
</td>
<td>
<i class="bi " [ngClass]="customEvent.isParticipationEvent ? 'bi-check-lg text-success' : 'bi-x-lg text-danger'"></i>
</td>
<td>
@if (customEvent.isInProgress) {
<i class="bi bi-hourglass-split"></i> In Progress
} @else {
Done
}
</td>
<td>{{customEvent.createdBy}}</td>
<td>
<div class="d-flex gap-3 justify-content-around">
<i (click)="onGoToCustomEventDetail(customEvent)" class="bi custom-info-icon bi-info-circle-fill"></i>
<i (click)="onEditCustomEvent(customEvent)" class="bi custom-edit-icon bi-pencil-fill"></i>
<i (click)="onDeleteCustomEvent(customEvent)" class="bi custom-delete-icon bi-trash3"></i>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
} @else {
<div class="alert alert-secondary text-center mt-5" role="alert">
No saved custom events
</div>
}
}
</div>

View File

@ -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<string>(isUpdate ? customEventDetail!.id : ''),
allianceId: new FormControl<string>(this.allianceId),
name: new FormControl<string>(isUpdate ? customEventDetail!.name : '', [Validators.required, Validators.maxLength(150)]),
description: new FormControl<string>(isUpdate ? customEventDetail!.description : '', [Validators.required, Validators.maxLength(500)]),
isPointsEvent: new FormControl<boolean>(isUpdate ? customEventDetail!.isPointsEvent : false),
isParticipationEvent: new FormControl(isUpdate ? customEventDetail!.isParticipationEvent : false),
eventDate: new FormControl<string>(new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())).toISOString().substring(0, 10)),
isInProgress: new FormControl<boolean>(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<CustomEventParticipantModel>[] = [];
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);
}
})
})
}
}

View File

@ -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<void> {
return this._httpClient.post<void>(this._serviceUrl, customEventParticipants);
}
updateCustomEventParticipant(customEventParticipantId: string, customEventParticipantModel: CustomEventParticipantModel): Observable<CustomEventParticipantModel> {
return this._httpClient.put<CustomEventParticipantModel>(this._serviceUrl + customEventParticipantId, customEventParticipantModel);
}
}

View File

@ -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<CustomEventModel[]> {
let params = new HttpParams();
params = params.append('take', take);
return this._httpClient.get<CustomEventModel[]>(this._serviceUrl + 'Alliance/' + allianceId, {params: params});
}
getCustomEventDetail(customEventId: string): Observable<CustomEventDetailModel> {
return this._httpClient.get<CustomEventDetailModel>(this._serviceUrl + 'GetCustomEventDetail/' + customEventId);
}
createCustomEvent(customEvent: CreateCustomEventModel): Observable<CustomEventModel> {
return this._httpClient.post<CustomEventModel>(this._serviceUrl, customEvent);
}
updateCustomEvent(customEventId: string, customEvent: CustomEventModel): Observable<CustomEventModel> {
return this._httpClient.put<CustomEventModel>(this._serviceUrl + customEventId, customEvent);
}
deleteCustomEvent(customEventId: string): Observable<boolean> {
return this._httpClient.delete<boolean>(this._serviceUrl + customEventId);
}
}