mirror of
https://github.com/TomasiDeveloping/PlayerManagement.git
synced 2026-04-16 09:12:20 +00:00
v 0.9.0
This commit is contained in:
parent
c48d8d8211
commit
711fb4adaa
134
Api/Controllers/v1/CustomEventCategoriesController.cs
Normal file
134
Api/Controllers/v1/CustomEventCategoriesController.cs
Normal file
@ -0,0 +1,134 @@
|
||||
using Application.DataTransferObjects.CustomEventCategory;
|
||||
using Application.Errors;
|
||||
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 CustomEventCategoriesController(ICustomEventCategoryRepository customEventCategoryRepository, ILogger<CustomEventCategoriesController> logger) : ControllerBase
|
||||
{
|
||||
[HttpGet("{customEventCategoryId:guid}")]
|
||||
public async Task<ActionResult<CustomEventCategoryDto>> GetCustomEventCategory(Guid customEventCategoryId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var customEventCategoryResult =
|
||||
await customEventCategoryRepository.GetCustomEventCategoryAsync(customEventCategoryId, cancellationToken);
|
||||
|
||||
return customEventCategoryResult.IsFailure
|
||||
? BadRequest(customEventCategoryResult.Error)
|
||||
: Ok(customEventCategoryResult.Value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "{ErrorMessage}", e.Message);
|
||||
return Problem(
|
||||
detail: $"Failed to process {nameof(GetCustomEventCategory)}",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Internal server error");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("Alliance/{allianceId:guid}")]
|
||||
public async Task<ActionResult<List<CustomEventCategoryDto>>> GetAllianceCustomEventCategories(Guid allianceId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allianceCustomEventCategoriesResult =
|
||||
await customEventCategoryRepository.GetAllianceCustomEventCategoriesAsync(allianceId, cancellationToken);
|
||||
|
||||
if (allianceCustomEventCategoriesResult.IsFailure) return BadRequest(allianceCustomEventCategoriesResult.Error);
|
||||
|
||||
return allianceCustomEventCategoriesResult.Value.Count > 0
|
||||
? Ok(allianceCustomEventCategoriesResult.Value)
|
||||
: NoContent();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "{ErrorMessage}", e.Message);
|
||||
return Problem(
|
||||
detail: $"Failed to process {nameof(GetAllianceCustomEventCategories)}",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Internal server error");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<CustomEventCategoryDto>> CreateCustomEventCategory(CreateCustomEventCategoryDto createCustomEventCategoryDto,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ModelState.IsValid) return UnprocessableEntity(ModelState);
|
||||
|
||||
var createResult = await customEventCategoryRepository.CreateCustomEventCategoryAsync(createCustomEventCategoryDto, cancellationToken);
|
||||
|
||||
return createResult.IsFailure
|
||||
? BadRequest(createResult.Error)
|
||||
: CreatedAtAction(nameof(GetCustomEventCategory), new { customEventCategoryId = createResult.Value.Id },
|
||||
createResult.Value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "{ErrorMessage}", e.Message);
|
||||
return Problem(
|
||||
detail: $"Failed to process {nameof(CreateCustomEventCategory)}",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Internal server error");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{customEventCategoryId:guid}")]
|
||||
public async Task<ActionResult<CustomEventCategoryDto>> UpdateCustomEventCategory(Guid customEventCategoryId,
|
||||
UpdateCustomEventCategoryDto updateCustomEventCategoryDto, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ModelState.IsValid) return UnprocessableEntity(ModelState);
|
||||
|
||||
if (updateCustomEventCategoryDto.Id != customEventCategoryId) return Conflict(CustomEventCategoryErrors.IdConflict);
|
||||
|
||||
var updateResult = await customEventCategoryRepository.UpdateCustomEventCategoryAsync(updateCustomEventCategoryDto, cancellationToken);
|
||||
|
||||
return updateResult.IsFailure
|
||||
? BadRequest(updateResult.Error)
|
||||
: Ok(updateResult.Value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "{ErrorMessage}", e.Message);
|
||||
return Problem(
|
||||
detail: $"Failed to process {nameof(UpdateCustomEventCategory)}",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Internal server error");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{customEventCategoryId:guid}")]
|
||||
public async Task<ActionResult<bool>> DeleteCustomEventCategory(Guid customEventCategoryId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleteResult = await customEventCategoryRepository.DeleteCustomEventAsync(customEventCategoryId, cancellationToken);
|
||||
|
||||
return deleteResult.IsFailure
|
||||
? BadRequest(deleteResult.Error)
|
||||
: Ok(deleteResult.Value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "{ErrorMessage}", e.Message);
|
||||
return Problem(
|
||||
detail: $"Failed to process {nameof(DeleteCustomEventCategory)}",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Internal server error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
Api/Controllers/v1/CustomEventLeaderboardsController.cs
Normal file
83
Api/Controllers/v1/CustomEventLeaderboardsController.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using Application.DataTransferObjects.CustomEventLeaderboard;
|
||||
using Application.Repositories;
|
||||
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 CustomEventLeaderboardsController(ICustomEventLeaderBoardRepository customEventLeaderBoardRepository, ILogger<CustomEventLeaderboardsController> logger) : ControllerBase
|
||||
{
|
||||
[HttpGet("point/{customEventCategoryId:guid}")]
|
||||
public async Task<ActionResult<LeaderboardPointEventDto>> GetPointEvent(Guid customEventCategoryId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pointEventLeaderboardResult = await customEventLeaderBoardRepository.GetPointEventLeaderboardAsync(customEventCategoryId, cancellationToken);
|
||||
|
||||
if (pointEventLeaderboardResult.IsFailure) return BadRequest(pointEventLeaderboardResult.Error);
|
||||
|
||||
return pointEventLeaderboardResult.Value.Count > 0
|
||||
? Ok(pointEventLeaderboardResult.Value)
|
||||
: NoContent();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "{ErrorMessage}", e.Message);
|
||||
return Problem(
|
||||
detail: $"Failed to process {nameof(GetPointEvent)}",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Internal server error");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("participation/{customEventCategoryId:guid}")]
|
||||
public async Task<ActionResult<LeaderboardParticipationEventDto>> GetParticipationEvent(Guid customEventCategoryId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var participationEventLeaderboardResult = await customEventLeaderBoardRepository.GetParticipationEventLeaderboardAsync(customEventCategoryId, cancellationToken);
|
||||
if (participationEventLeaderboardResult.IsFailure) return BadRequest(participationEventLeaderboardResult.Error);
|
||||
return participationEventLeaderboardResult.Value.Count > 0
|
||||
? Ok(participationEventLeaderboardResult.Value)
|
||||
: NoContent();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "{ErrorMessage}", e.Message);
|
||||
return Problem(
|
||||
detail: $"Failed to process {nameof(GetParticipationEvent)}",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Internal server error");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("point-and-participation/{customEventCategoryId:guid}")]
|
||||
public async Task<ActionResult<LeaderboardPointAndParticipationEventDto>> GetPointAndParticipationEvent(Guid customEventCategoryId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pointAndParticipationEventLeaderboardResult = await customEventLeaderBoardRepository.GetPointAndParticipationEventLeaderboardAsync(customEventCategoryId, cancellationToken);
|
||||
if (pointAndParticipationEventLeaderboardResult.IsFailure) return BadRequest(pointAndParticipationEventLeaderboardResult.Error);
|
||||
return pointAndParticipationEventLeaderboardResult.Value.Count > 0
|
||||
? Ok(pointAndParticipationEventLeaderboardResult.Value)
|
||||
: NoContent();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "{ErrorMessage}", e.Message);
|
||||
return Problem(
|
||||
detail: $"Failed to process {nameof(GetPointAndParticipationEvent)}",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Internal server error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -34,6 +34,8 @@ public static class ApplicationDependencyInjection
|
||||
services.AddScoped<IZombieSiegeParticipantRepository, ZombieSiegeParticipantRepository>();
|
||||
services.AddScoped<IVsDuelLeagueRepository, VsDuelLeagueRepository>();
|
||||
services.AddScoped<IApiKeyRepository, ApiKeyRepository>();
|
||||
services.AddScoped<ICustomEventCategoryRepository, CustomEventCategoryRepository>();
|
||||
services.AddScoped<ICustomEventLeaderBoardRepository, CustomEventLeaderboardRepository>();
|
||||
|
||||
|
||||
services.AddTransient<IJwtService, JwtService>();
|
||||
|
||||
@ -21,6 +21,8 @@ public class CreateCustomEventDto
|
||||
[Required]
|
||||
public Guid AllianceId { get; set; }
|
||||
|
||||
public Guid? CustomEventCategoryId { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string EventDate { get; set; }
|
||||
|
||||
|
||||
@ -6,6 +6,10 @@ public class CustomEventDto
|
||||
|
||||
public Guid AllianceId { get; set; }
|
||||
|
||||
public Guid? CustomEventCategoryId { get; set; }
|
||||
|
||||
public string? CategoryName { get; set; }
|
||||
|
||||
public required string Name { get; set; }
|
||||
|
||||
public required string Description { get; set; }
|
||||
|
||||
@ -7,6 +7,8 @@ public class UpdateCustomEventDto
|
||||
[Required]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid? CustomEventCategoryId { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(150)]
|
||||
public required string Name { get; set; }
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DataTransferObjects.CustomEventCategory;
|
||||
|
||||
public class CreateCustomEventCategoryDto
|
||||
{
|
||||
[Required]
|
||||
public Guid AllianceId { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(250)]
|
||||
public required string Name { get; set; }
|
||||
|
||||
[Required]
|
||||
public bool IsPointsEvent { get; set; }
|
||||
|
||||
[Required]
|
||||
public bool IsParticipationEvent { get; set; }
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
namespace Application.DataTransferObjects.CustomEventCategory;
|
||||
|
||||
public class CustomEventCategoryDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid AllianceId { get; set; }
|
||||
|
||||
public required string Name { get; set; }
|
||||
|
||||
public bool IsPointsEvent { get; set; }
|
||||
|
||||
public bool IsParticipationEvent { get; set; }
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Application.DataTransferObjects.CustomEventCategory;
|
||||
|
||||
public class UpdateCustomEventCategoryDto
|
||||
{
|
||||
[Required]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(250)]
|
||||
public required string Name { get; set; }
|
||||
|
||||
[Required]
|
||||
public bool IsPointsEvent { get; set; }
|
||||
|
||||
[Required]
|
||||
public bool IsParticipationEvent { get; set; }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Application.DataTransferObjects.CustomEventLeaderboard;
|
||||
|
||||
public class LeaderboardParticipationEventDto
|
||||
{
|
||||
public required string PlayerName { get; set; }
|
||||
|
||||
public int Participations { get; set; }
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
namespace Application.DataTransferObjects.CustomEventLeaderboard;
|
||||
|
||||
public class LeaderboardPointAndParticipationEventDto
|
||||
{
|
||||
public required string PlayerName { get; set; }
|
||||
|
||||
public long Points { get; set; }
|
||||
|
||||
public int Participations { get; set; }
|
||||
|
||||
public double TotalPoints { get; set; }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Application.DataTransferObjects.CustomEventLeaderboard;
|
||||
|
||||
public class LeaderboardPointEventDto
|
||||
{
|
||||
public required string PlayerName { get; set; }
|
||||
|
||||
public long Points { get; set; }
|
||||
}
|
||||
9
Application/Errors/CustomEventCategoryErrors.cs
Normal file
9
Application/Errors/CustomEventCategoryErrors.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Application.Errors;
|
||||
|
||||
public class CustomEventCategoryErrors
|
||||
{
|
||||
public static readonly Error NotFound = new("Error.CustomEventCategory.NotFound",
|
||||
"The custom event category with the specified identifier was not found");
|
||||
|
||||
public static readonly Error IdConflict = new("Error.CustomEventCategory.IdConflict", "There is a conflict with the id's");
|
||||
}
|
||||
17
Application/Interfaces/ICustomEventCategoryRepository.cs
Normal file
17
Application/Interfaces/ICustomEventCategoryRepository.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using Application.Classes;
|
||||
using Application.DataTransferObjects.CustomEventCategory;
|
||||
|
||||
namespace Application.Interfaces;
|
||||
|
||||
public interface ICustomEventCategoryRepository
|
||||
{
|
||||
Task<Result<CustomEventCategoryDto>> GetCustomEventCategoryAsync(Guid customEventCategoryId, CancellationToken cancellationToken);
|
||||
|
||||
Task<Result<List<CustomEventCategoryDto>>> GetAllianceCustomEventCategoriesAsync(Guid allianceId, CancellationToken cancellationToken);
|
||||
|
||||
Task<Result<CustomEventCategoryDto>> CreateCustomEventCategoryAsync(CreateCustomEventCategoryDto createCustomEventCategoryDto, CancellationToken cancellationToken);
|
||||
|
||||
Task<Result<CustomEventCategoryDto>> UpdateCustomEventCategoryAsync(UpdateCustomEventCategoryDto updateCustomEventCategoryDto, CancellationToken cancellationToken);
|
||||
|
||||
Task<Result<bool>> DeleteCustomEventAsync(Guid customEventId, CancellationToken cancellationToken);
|
||||
}
|
||||
18
Application/Profiles/CustomEventCategoryProfile.cs
Normal file
18
Application/Profiles/CustomEventCategoryProfile.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Application.DataTransferObjects.CustomEventCategory;
|
||||
using AutoMapper;
|
||||
using Database.Entities;
|
||||
|
||||
namespace Application.Profiles;
|
||||
|
||||
public class CustomEventCategoryProfile : Profile
|
||||
{
|
||||
public CustomEventCategoryProfile()
|
||||
{
|
||||
CreateMap<CustomEventCategory, CustomEventCategoryDto>();
|
||||
|
||||
CreateMap<CreateCustomEventCategoryDto, CustomEventCategory>()
|
||||
.ForMember(des => des.Id, opt => opt.MapFrom(src => Guid.CreateVersion7()));
|
||||
|
||||
CreateMap<UpdateCustomEventCategoryDto, CustomEventCategory>();
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,8 @@ public class CustomEventProfile : Profile
|
||||
{
|
||||
public CustomEventProfile()
|
||||
{
|
||||
CreateMap<CustomEvent, CustomEventDto>();
|
||||
CreateMap<CustomEvent, CustomEventDto>()
|
||||
.ForMember(des => des.CategoryName, opt => opt.MapFrom(src => src.CustomEventCategory!.Name));
|
||||
|
||||
CreateMap<CustomEvent, CustomEventDetailDto>()
|
||||
.ForMember(des => des.CustomEventParticipants, opt => opt.MapFrom(src => src.CustomEventParticipants));
|
||||
|
||||
111
Application/Repositories/CustomEventCategoryRepository.cs
Normal file
111
Application/Repositories/CustomEventCategoryRepository.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using Application.Classes;
|
||||
using Application.DataTransferObjects.CustomEventCategory;
|
||||
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 CustomEventCategoryRepository(ApplicationContext dbContext, IMapper mapper, ILogger<CustomEventCategoryRepository> logger) : ICustomEventCategoryRepository
|
||||
{
|
||||
public async Task<Result<CustomEventCategoryDto>> GetCustomEventCategoryAsync(Guid customEventCategoryId, CancellationToken cancellationToken)
|
||||
{
|
||||
var customEventCategoryById = await dbContext.CustomEventCategories
|
||||
.ProjectTo<CustomEventCategoryDto>(mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(customEventCategory => customEventCategory.Id == customEventCategoryId, cancellationToken);
|
||||
|
||||
return customEventCategoryById is null
|
||||
? Result.Failure<CustomEventCategoryDto>(CustomEventCategoryErrors.NotFound)
|
||||
: Result.Success(customEventCategoryById);
|
||||
}
|
||||
|
||||
public async Task<Result<List<CustomEventCategoryDto>>> GetAllianceCustomEventCategoriesAsync(Guid allianceId, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
var customEventCategories = await dbContext.CustomEventCategories
|
||||
.Where(customEventCategory => customEventCategory.AllianceId == allianceId)
|
||||
.ProjectTo<CustomEventCategoryDto>(mapper.ConfigurationProvider)
|
||||
.OrderByDescending(customEventCategory => customEventCategory.Id)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Result.Success(customEventCategories);
|
||||
}
|
||||
|
||||
public async Task<Result<CustomEventCategoryDto>> CreateCustomEventCategoryAsync(CreateCustomEventCategoryDto createCustomEventCategoryDto,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newCustomEventCategory = mapper.Map<CustomEventCategory>(createCustomEventCategoryDto);
|
||||
await dbContext.CustomEventCategories.AddAsync(newCustomEventCategory, cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
var customEventCategoryDto = mapper.Map<CustomEventCategoryDto>(newCustomEventCategory);
|
||||
return Result.Success(customEventCategoryDto);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "{ErrorMessage}", e.Message);
|
||||
return Result.Failure<CustomEventCategoryDto>(GeneralErrors.DatabaseError);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<CustomEventCategoryDto>> UpdateCustomEventCategoryAsync(UpdateCustomEventCategoryDto updateCustomEventCategoryDto,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var customEventCategoryToUpdate = await dbContext.CustomEventCategories
|
||||
.FirstOrDefaultAsync(customEventCategory => customEventCategory.Id == updateCustomEventCategoryDto.Id, cancellationToken);
|
||||
if (customEventCategoryToUpdate is null)
|
||||
{
|
||||
return Result.Failure<CustomEventCategoryDto>(CustomEventCategoryErrors.NotFound);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mapper.Map(updateCustomEventCategoryDto, customEventCategoryToUpdate);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
var customEventCategoryDto = mapper.Map<CustomEventCategoryDto>(customEventCategoryToUpdate);
|
||||
return Result.Success(customEventCategoryDto);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "{ErrorMessage}", e.Message);
|
||||
return Result.Failure<CustomEventCategoryDto>(GeneralErrors.DatabaseError);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<bool>> DeleteCustomEventAsync(Guid customEventId, CancellationToken cancellationToken)
|
||||
{
|
||||
var customEventToDelete = await dbContext.CustomEventCategories
|
||||
.FirstOrDefaultAsync(customEvent => customEvent.Id == customEventId, cancellationToken);
|
||||
if (customEventToDelete is null)
|
||||
{
|
||||
return Result.Failure<bool>(CustomEventCategoryErrors.NotFound);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var customEvents = await dbContext.CustomEvents
|
||||
.Where(customEvent => customEvent.CustomEventCategoryId == customEventToDelete.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
if (customEvents.Any())
|
||||
{
|
||||
dbContext.CustomEvents.RemoveRange(customEvents);
|
||||
}
|
||||
dbContext.CustomEventCategories.Remove(customEventToDelete);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "{ErrorMessage}", e.Message);
|
||||
return Result.Failure<bool>(GeneralErrors.DatabaseError);
|
||||
}
|
||||
}
|
||||
}
|
||||
155
Application/Repositories/CustomEventLeaderboardRepository.cs
Normal file
155
Application/Repositories/CustomEventLeaderboardRepository.cs
Normal file
@ -0,0 +1,155 @@
|
||||
using Application.Classes;
|
||||
using Application.DataTransferObjects.CustomEventLeaderboard;
|
||||
using Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Application.Repositories;
|
||||
|
||||
public class CustomEventLeaderboardRepository(ApplicationContext dbContext) : ICustomEventLeaderBoardRepository
|
||||
{
|
||||
public async Task<Result<List<LeaderboardPointEventDto>>> GetPointEventLeaderboardAsync(Guid customEventCategoryId, CancellationToken cancellationToken)
|
||||
{
|
||||
var category = await dbContext.CustomEventCategories
|
||||
.Where(category => category.Id == customEventCategoryId)
|
||||
.Include(c => c.CustomEvents)
|
||||
.ThenInclude(e => e.CustomEventParticipants)
|
||||
.ThenInclude(p => p.Player)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (category == null)
|
||||
{
|
||||
return Result.Success(new List<LeaderboardPointEventDto>());
|
||||
}
|
||||
|
||||
var leaderboard = category.CustomEvents
|
||||
.SelectMany(e => e.CustomEventParticipants)
|
||||
.GroupBy(p => new { p.Player.Id, p.Player.PlayerName })
|
||||
.Select(g => new LeaderboardPointEventDto
|
||||
{
|
||||
PlayerName = g.Key.PlayerName,
|
||||
Points = g.Sum(p => p.AchievedPoints ?? 0)
|
||||
})
|
||||
.OrderByDescending(l => l.Points)
|
||||
.ToList();
|
||||
|
||||
return Result.Success(leaderboard);
|
||||
}
|
||||
|
||||
public async Task<Result<List<LeaderboardParticipationEventDto>>> GetParticipationEventLeaderboardAsync(Guid customEventCategoryId, CancellationToken cancellationToken)
|
||||
{
|
||||
var category = await dbContext.CustomEventCategories
|
||||
.Where(category => category.Id == customEventCategoryId)
|
||||
.Include(c => c.CustomEvents)
|
||||
.ThenInclude(e => e.CustomEventParticipants)
|
||||
.ThenInclude(p => p.Player)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (category == null)
|
||||
{
|
||||
return Result.Success(new List<LeaderboardParticipationEventDto>());
|
||||
}
|
||||
|
||||
var leaderboard = category.CustomEvents
|
||||
.SelectMany(e => e.CustomEventParticipants)
|
||||
.GroupBy(p => new { p.Player.Id, p.Player.PlayerName })
|
||||
.Select(g => new LeaderboardParticipationEventDto()
|
||||
{
|
||||
PlayerName = g.Key.PlayerName,
|
||||
Participations = g.Count(z => z.Participated!.Value)
|
||||
})
|
||||
.OrderByDescending(l => l.Participations)
|
||||
.ToList();
|
||||
|
||||
return Result.Success(leaderboard);
|
||||
}
|
||||
|
||||
public async Task<Result<List<LeaderboardPointAndParticipationEventDto>>> GetPointAndParticipationEventLeaderboardAsync(Guid customEventCategoryId, CancellationToken cancellationToken)
|
||||
{
|
||||
var category = await dbContext.CustomEventCategories
|
||||
.Where(category => category.Id == customEventCategoryId)
|
||||
.Include(c => c.CustomEvents)
|
||||
.ThenInclude(e => e.CustomEventParticipants)
|
||||
.ThenInclude(p => p.Player)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (category == null)
|
||||
return Result.Success(new List<LeaderboardPointAndParticipationEventDto>());
|
||||
|
||||
// Punktetabelle: Platz 1 = 100, Platz 2 = 99, ..., Platz 100 = 1
|
||||
var pointTable = Enumerable.Range(1, 100).Select(i => 101 - i).ToArray();
|
||||
|
||||
// Gewichtungen
|
||||
const double baseParticipationValue = 10.0;
|
||||
const double placeWeight = 2.0;
|
||||
const double scoreWeight = 1.0;
|
||||
|
||||
// Dictionaries zur Akkumulation
|
||||
var playerPoints = new Dictionary<Guid, long>();
|
||||
var playerParticipationCount = new Dictionary<Guid, int>();
|
||||
var playerTotalPoints = new Dictionary<Guid, double>();
|
||||
var playerNames = new Dictionary<Guid, string>();
|
||||
|
||||
foreach (var ev in category.CustomEvents)
|
||||
{
|
||||
// Vorab Platzierung berechnen
|
||||
var ranked = ev.CustomEventParticipants
|
||||
.Where(p => p.Participated == true)
|
||||
.OrderByDescending(p => p.AchievedPoints ?? 0)
|
||||
.ToList();
|
||||
|
||||
foreach (var participant in ev.CustomEventParticipants)
|
||||
{
|
||||
if (participant.Player == null) continue;
|
||||
|
||||
var playerId = participant.Player.Id;
|
||||
var playerName = participant.Player.PlayerName;
|
||||
|
||||
// Spielername merken – egal ob teilgenommen oder nicht
|
||||
playerNames[playerId] = playerName;
|
||||
|
||||
// Nur wenn teilgenommen
|
||||
if (participant.Participated == true)
|
||||
{
|
||||
var score = participant.AchievedPoints ?? 0;
|
||||
|
||||
// Index in Platzierungsliste finden
|
||||
var place = ranked.FindIndex(p => p.Player?.Id == playerId);
|
||||
var placePoints = (place >= 0 && place < pointTable.Length) ? pointTable[place] : 1;
|
||||
var normalizedPlace = placePoints / 100.0;
|
||||
var scoreBonus = Math.Log10(score + 1);
|
||||
|
||||
var total = baseParticipationValue + (normalizedPlace * placeWeight) + (scoreBonus * scoreWeight);
|
||||
|
||||
playerTotalPoints[playerId] = playerTotalPoints.GetValueOrDefault(playerId) + total;
|
||||
playerParticipationCount[playerId] = playerParticipationCount.GetValueOrDefault(playerId) + 1;
|
||||
playerPoints[playerId] = playerPoints.GetValueOrDefault(playerId) + score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Leaderboard für alle Spieler, die in Events vorkommen
|
||||
var leaderboard = playerNames.Select(entry =>
|
||||
{
|
||||
var playerId = entry.Key;
|
||||
var playerName = entry.Value;
|
||||
|
||||
return new LeaderboardPointAndParticipationEventDto
|
||||
{
|
||||
PlayerName = playerName,
|
||||
Points = playerPoints.GetValueOrDefault(playerId),
|
||||
Participations = playerParticipationCount.GetValueOrDefault(playerId),
|
||||
TotalPoints = Math.Round(playerTotalPoints.GetValueOrDefault(playerId), 2)
|
||||
};
|
||||
})
|
||||
.OrderByDescending(x => x.TotalPoints)
|
||||
.ThenByDescending(x => x.Participations)
|
||||
.ToList();
|
||||
|
||||
return Result.Success(leaderboard);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
using Application.Classes;
|
||||
using Application.DataTransferObjects.CustomEventLeaderboard;
|
||||
|
||||
|
||||
namespace Application.Repositories;
|
||||
|
||||
public interface ICustomEventLeaderBoardRepository
|
||||
{
|
||||
Task<Result<List<LeaderboardPointEventDto>>> GetPointEventLeaderboardAsync(Guid customEventCategoryId, CancellationToken cancellationToken);
|
||||
|
||||
Task<Result<List<LeaderboardParticipationEventDto>>> GetParticipationEventLeaderboardAsync(Guid customEventCategoryId, CancellationToken cancellationToken);
|
||||
|
||||
Task<Result<List<LeaderboardPointAndParticipationEventDto>>> GetPointAndParticipationEventLeaderboardAsync(Guid customEventCategoryId, CancellationToken cancellationToken);
|
||||
}
|
||||
@ -44,6 +44,8 @@ public class ApplicationContext(DbContextOptions<ApplicationContext> options) :
|
||||
|
||||
public DbSet<ApiKey> ApiKeys { get; set; }
|
||||
|
||||
public DbSet<CustomEventCategory> CustomEventCategories { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
24
Database/Configurations/CustomEventCategoryConfiguration.cs
Normal file
24
Database/Configurations/CustomEventCategoryConfiguration.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Database.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Database.Configurations;
|
||||
|
||||
public class CustomEventCategoryConfiguration : IEntityTypeConfiguration<CustomEventCategory>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<CustomEventCategory> builder)
|
||||
{
|
||||
builder.HasKey(customEventCategory => customEventCategory.Id);
|
||||
builder.Property(customEventCategory => customEventCategory.Id).ValueGeneratedNever();
|
||||
|
||||
builder.Property(customEventCategory => customEventCategory.AllianceId).IsRequired();
|
||||
builder.Property(customEventCategory => customEventCategory.Name).IsRequired().HasMaxLength(255);
|
||||
builder.Property(customEventCategory => customEventCategory.IsPointsEvent).IsRequired();
|
||||
builder.Property(customEventCategory => customEventCategory.IsParticipationEvent).IsRequired();
|
||||
|
||||
builder.HasOne(customEventCategory => customEventCategory.Alliance)
|
||||
.WithMany(alliance => alliance.CustomEventCategories)
|
||||
.HasForeignKey(customEventCategory => customEventCategory.AllianceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@ -25,5 +25,10 @@ public class CustomEventConfiguration : IEntityTypeConfiguration<CustomEvent>
|
||||
.WithMany(a => a.CustomEvents)
|
||||
.HasForeignKey(c => c.AllianceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(customEvent => customEvent.CustomEventCategory)
|
||||
.WithMany(customEventCategory => customEventCategory.CustomEvents)
|
||||
.HasForeignKey(customEvent => customEvent.CustomEventCategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@ -29,4 +29,6 @@ public class Alliance : BaseEntity
|
||||
public ICollection<VsDuel> VsDuels { get; set; } = [];
|
||||
|
||||
public ICollection<ZombieSiege> ZombieSieges { get; set; } = [];
|
||||
|
||||
public ICollection<CustomEventCategory> CustomEventCategories { get; set; } = [];
|
||||
}
|
||||
@ -6,6 +6,10 @@ public class CustomEvent : BaseEntity
|
||||
|
||||
public Alliance Alliance { get; set; } = null!;
|
||||
|
||||
public Guid? CustomEventCategoryId { get; set; }
|
||||
|
||||
public CustomEventCategory? CustomEventCategory { get; set; }
|
||||
|
||||
public required string Name { get; set; }
|
||||
|
||||
public required string Description { get; set; }
|
||||
|
||||
16
Database/Entities/CustomEventCategory.cs
Normal file
16
Database/Entities/CustomEventCategory.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace Database.Entities;
|
||||
|
||||
public class CustomEventCategory : BaseEntity
|
||||
{
|
||||
public Guid AllianceId { get; set; }
|
||||
|
||||
public Alliance Alliance { get; set; } = null!;
|
||||
|
||||
public required string Name { get; set; }
|
||||
|
||||
public bool IsPointsEvent { get; set; }
|
||||
|
||||
public bool IsParticipationEvent { get; set; }
|
||||
|
||||
public ICollection<CustomEvent> CustomEvents { get; set; } = [];
|
||||
}
|
||||
1286
Database/Migrations/20250417062713_Add_CustomEventCategory.Designer.cs
generated
Normal file
1286
Database/Migrations/20250417062713_Add_CustomEventCategory.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,93 @@
|
||||
//<auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_CustomEventCategory : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "CustomEventCategoryId",
|
||||
schema: "dbo",
|
||||
table: "CustomEvents",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CustomEventCategory",
|
||||
schema: "dbo",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
AllianceId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
|
||||
IsPointsEvent = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsParticipationEvent = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CustomEventCategory", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CustomEventCategory_Alliances_AllianceId",
|
||||
column: x => x.AllianceId,
|
||||
principalSchema: "dbo",
|
||||
principalTable: "Alliances",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CustomEvents_CustomEventCategoryId",
|
||||
schema: "dbo",
|
||||
table: "CustomEvents",
|
||||
column: "CustomEventCategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CustomEventCategory_AllianceId",
|
||||
schema: "dbo",
|
||||
table: "CustomEventCategory",
|
||||
column: "AllianceId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_CustomEvents_CustomEventCategory_CustomEventCategoryId",
|
||||
schema: "dbo",
|
||||
table: "CustomEvents",
|
||||
column: "CustomEventCategoryId",
|
||||
principalSchema: "dbo",
|
||||
principalTable: "CustomEventCategory",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_CustomEvents_CustomEventCategory_CustomEventCategoryId",
|
||||
schema: "dbo",
|
||||
table: "CustomEvents");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CustomEventCategory",
|
||||
schema: "dbo");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_CustomEvents_CustomEventCategoryId",
|
||||
schema: "dbo",
|
||||
table: "CustomEvents");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CustomEventCategoryId",
|
||||
schema: "dbo",
|
||||
table: "CustomEvents");
|
||||
}
|
||||
}
|
||||
}
|
||||
1286
Database/Migrations/20250417071312_Update_CustomEventCategory.Designer.cs
generated
Normal file
1286
Database/Migrations/20250417071312_Update_CustomEventCategory.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
128
Database/Migrations/20250417071312_Update_CustomEventCategory.cs
Normal file
128
Database/Migrations/20250417071312_Update_CustomEventCategory.cs
Normal file
@ -0,0 +1,128 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Update_CustomEventCategory : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_CustomEventCategory_Alliances_AllianceId",
|
||||
schema: "dbo",
|
||||
table: "CustomEventCategory");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_CustomEvents_CustomEventCategory_CustomEventCategoryId",
|
||||
schema: "dbo",
|
||||
table: "CustomEvents");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_CustomEventCategory",
|
||||
schema: "dbo",
|
||||
table: "CustomEventCategory");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "CustomEventCategory",
|
||||
schema: "dbo",
|
||||
newName: "CustomEventCategories",
|
||||
newSchema: "dbo");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_CustomEventCategory_AllianceId",
|
||||
schema: "dbo",
|
||||
table: "CustomEventCategories",
|
||||
newName: "IX_CustomEventCategories_AllianceId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_CustomEventCategories",
|
||||
schema: "dbo",
|
||||
table: "CustomEventCategories",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_CustomEventCategories_Alliances_AllianceId",
|
||||
schema: "dbo",
|
||||
table: "CustomEventCategories",
|
||||
column: "AllianceId",
|
||||
principalSchema: "dbo",
|
||||
principalTable: "Alliances",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_CustomEvents_CustomEventCategories_CustomEventCategoryId",
|
||||
schema: "dbo",
|
||||
table: "CustomEvents",
|
||||
column: "CustomEventCategoryId",
|
||||
principalSchema: "dbo",
|
||||
principalTable: "CustomEventCategories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_CustomEventCategories_Alliances_AllianceId",
|
||||
schema: "dbo",
|
||||
table: "CustomEventCategories");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_CustomEvents_CustomEventCategories_CustomEventCategoryId",
|
||||
schema: "dbo",
|
||||
table: "CustomEvents");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_CustomEventCategories",
|
||||
schema: "dbo",
|
||||
table: "CustomEventCategories");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "CustomEventCategories",
|
||||
schema: "dbo",
|
||||
newName: "CustomEventCategory",
|
||||
newSchema: "dbo");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_CustomEventCategories_AllianceId",
|
||||
schema: "dbo",
|
||||
table: "CustomEventCategory",
|
||||
newName: "IX_CustomEventCategory_AllianceId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_CustomEventCategory",
|
||||
schema: "dbo",
|
||||
table: "CustomEventCategory",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_CustomEventCategory_Alliances_AllianceId",
|
||||
schema: "dbo",
|
||||
table: "CustomEventCategory",
|
||||
column: "AllianceId",
|
||||
principalSchema: "dbo",
|
||||
principalTable: "Alliances",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_CustomEvents_CustomEventCategory_CustomEventCategoryId",
|
||||
schema: "dbo",
|
||||
table: "CustomEvents",
|
||||
column: "CustomEventCategoryId",
|
||||
principalSchema: "dbo",
|
||||
principalTable: "CustomEventCategory",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -19,7 +18,7 @@ namespace Database.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("dbo")
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("ProductVersion", "9.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
@ -140,6 +139,9 @@ namespace Database.Migrations
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("nvarchar(150)");
|
||||
|
||||
b.Property<Guid?>("CustomEventCategoryId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
@ -173,9 +175,37 @@ namespace Database.Migrations
|
||||
|
||||
b.HasIndex("AllianceId");
|
||||
|
||||
b.HasIndex("CustomEventCategoryId");
|
||||
|
||||
b.ToTable("CustomEvents", "dbo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Database.Entities.CustomEventCategory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AllianceId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsParticipationEvent")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsPointsEvent")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("nvarchar(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AllianceId");
|
||||
|
||||
b.ToTable("CustomEventCategories", "dbo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Database.Entities.CustomEventParticipant", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -624,19 +654,19 @@ namespace Database.Migrations
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = new Guid("0194d053-12b9-7818-9417-4d4eaa3b5ec1"),
|
||||
Id = new Guid("01964298-2d6d-7f54-8e1f-ab23ec343378"),
|
||||
Code = 1,
|
||||
Name = "Silver League"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("0194d053-12b9-7c64-9efb-e88745510c35"),
|
||||
Id = new Guid("01964298-2d6d-7620-bcc5-08103cbfd5de"),
|
||||
Code = 2,
|
||||
Name = "Gold League"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("0194d053-12b9-7217-a040-88ba1bbc7d69"),
|
||||
Id = new Guid("01964298-2d6d-7e0b-b002-532458cbe7bd"),
|
||||
Code = 3,
|
||||
Name = "Diamond League"
|
||||
});
|
||||
@ -911,6 +941,24 @@ namespace Database.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Database.Entities.CustomEventCategory", "CustomEventCategory")
|
||||
.WithMany("CustomEvents")
|
||||
.HasForeignKey("CustomEventCategoryId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Alliance");
|
||||
|
||||
b.Navigation("CustomEventCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Database.Entities.CustomEventCategory", b =>
|
||||
{
|
||||
b.HasOne("Database.Entities.Alliance", "Alliance")
|
||||
.WithMany("CustomEventCategories")
|
||||
.HasForeignKey("AllianceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Alliance");
|
||||
});
|
||||
|
||||
@ -1156,6 +1204,8 @@ namespace Database.Migrations
|
||||
{
|
||||
b.Navigation("ApiKey");
|
||||
|
||||
b.Navigation("CustomEventCategories");
|
||||
|
||||
b.Navigation("CustomEvents");
|
||||
|
||||
b.Navigation("DesertStorms");
|
||||
@ -1176,6 +1226,11 @@ namespace Database.Migrations
|
||||
b.Navigation("CustomEventParticipants");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Database.Entities.CustomEventCategory", b =>
|
||||
{
|
||||
b.Navigation("CustomEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Database.Entities.DesertStorm", b =>
|
||||
{
|
||||
b.Navigation("DesertStormParticipants");
|
||||
|
||||
17
README.md
17
README.md
@ -17,15 +17,28 @@ All notable changes to this project are documented here.
|
||||
This project is currently in the **Beta Phase**.
|
||||
|
||||
---
|
||||
|
||||
### **[0.9.0]** - *2025-04-23*
|
||||
#### ✨ Added
|
||||
- **Event Categories:** Custom events can now be assigned to specific categories.
|
||||
- **Category-Based Leaderboards:** Each category can have its own leaderboard, depending on the event type (e.g., participation-only, points-only, or a combination).
|
||||
|
||||
🛠️ **Fixed**
|
||||
- Minor display issues in the event and leaderboard sections have been resolved.
|
||||
|
||||
---
|
||||
|
||||
### **[0.8.3]** - *2025-04-10*
|
||||
#### ♻️ Changed
|
||||
- **NuGet Packages:** Updated all dependencies, including Serilog and Seq, to their latest stable versions.
|
||||
- **Logging Setup:** Cleaned up and reorganized the Serilog configuration for improved clarity and maintainability.
|
||||
|
||||
---
|
||||
|
||||
🛠️ **Fixed**
|
||||
- (N/A)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### **[0.8.0]** - *2025-03-11*
|
||||
#### ✨ Added
|
||||
- **Feedback Page:** Users can now submit feedback, including bug reports and feature requests.
|
||||
|
||||
@ -61,6 +61,9 @@ import { AllianceApiKeyComponent } from './pages/alliance/alliance-api-key/allia
|
||||
import { AllianceUserAdministrationComponent } from './pages/alliance/alliance-user-administration/alliance-user-administration.component';
|
||||
import { FeedbackComponent } from './pages/feedback/feedback.component';
|
||||
import { ImprintComponent } from './pages/imprint/imprint.component';
|
||||
import { CustomEventCategoryComponent } from './pages/custom-event/custom-event-category/custom-event-category.component';
|
||||
import { CustomEventLeaderboardComponent } from './pages/custom-event/custom-event-leaderboard/custom-event-leaderboard.component';
|
||||
import { CustomEventEventsComponent } from './pages/custom-event/custom-event-events/custom-event-events.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -111,7 +114,10 @@ import { ImprintComponent } from './pages/imprint/imprint.component';
|
||||
AllianceApiKeyComponent,
|
||||
AllianceUserAdministrationComponent,
|
||||
FeedbackComponent,
|
||||
ImprintComponent
|
||||
ImprintComponent,
|
||||
CustomEventCategoryComponent,
|
||||
CustomEventLeaderboardComponent,
|
||||
CustomEventEventsComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
||||
@ -4,6 +4,8 @@ export interface CustomEventModel {
|
||||
id: string;
|
||||
allianceId: string;
|
||||
name: string;
|
||||
categoryName?: string;
|
||||
customEventCategoryId?: string;
|
||||
description: string;
|
||||
isPointsEvent: boolean;
|
||||
isParticipationEvent: boolean;
|
||||
@ -25,5 +27,6 @@ export interface CreateCustomEventModel {
|
||||
isParticipationEvent: boolean;
|
||||
eventDate: string;
|
||||
allianceId: string;
|
||||
customEventCategoryId?: string;
|
||||
isInProgress: boolean;
|
||||
}
|
||||
|
||||
21
Ui/src/app/models/customEventCategory.model.ts
Normal file
21
Ui/src/app/models/customEventCategory.model.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export interface CustomEventCategoryModel {
|
||||
id: string;
|
||||
allianceId: string;
|
||||
name: string;
|
||||
isPointsEvent: boolean;
|
||||
isParticipationEvent: boolean;
|
||||
}
|
||||
|
||||
export interface CreateCustomEventCategoryModel {
|
||||
allianceId: string;
|
||||
name: string;
|
||||
isPointsEvent: boolean;
|
||||
isParticipationEvent: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateCustomEventCategoryModel {
|
||||
id: string;
|
||||
name: string;
|
||||
isPointsEvent: boolean;
|
||||
isParticipationEvent: boolean;
|
||||
}
|
||||
16
Ui/src/app/models/customEventLeaderboard.model.ts
Normal file
16
Ui/src/app/models/customEventLeaderboard.model.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface LeaderboardPointEventModel {
|
||||
playerName: string;
|
||||
points: number;
|
||||
}
|
||||
|
||||
export interface LeaderboardParticipationEventModel {
|
||||
playerName: string;
|
||||
participations: number;
|
||||
}
|
||||
|
||||
export interface LeaderboardPointAndParticipationEventModel {
|
||||
playerName: string;
|
||||
participations: number;
|
||||
points: number;
|
||||
totalPoints: number;
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
@if (!isCreateCustomEventCategory) {
|
||||
<div class="d-grid gap-2 col-6 mx-auto">
|
||||
<button (click)="onCreateCategory()" class="btn btn-primary" type="button">Create new Category</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isCreateCustomEventCategory) {
|
||||
<form [formGroup]="customEventCategoryForm">
|
||||
<!-- Event name-->
|
||||
<div class="form-floating mb-3 is-invalid">
|
||||
<input [ngClass]="{
|
||||
'is-invalid': f['name'].invalid && (f['name'].dirty || !f['name'].untouched),
|
||||
'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'].untouched)) {
|
||||
<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>
|
||||
<!-- 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>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<button (click)="onCancel()" type="button" class="btn btn-warning">Cancel</button>
|
||||
<button [disabled]="customEventCategoryForm.invalid" (click)="onSubmit()" type="submit" class="btn btn-success">{{isUpdate ? 'Update': 'Create'}}</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (!isCreateCustomEventCategory) {
|
||||
@if (customEventCategories.length > 0) {
|
||||
<div class="table-responsive mt-5">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Point event</th>
|
||||
<th scope="col">Participation event</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (customEventCategory of customEventCategories | paginate: { id: 'customEventTable', itemsPerPage: 5, currentPage: pageNumber}; track customEventCategory.id) {
|
||||
<tr>
|
||||
<td>{{customEventCategory.name}}</td>
|
||||
<td>
|
||||
<i class="bi " [ngClass]="customEventCategory.isPointsEvent ? 'bi-check-lg text-success' : 'bi-x-lg text-danger'"></i>
|
||||
</td>
|
||||
<td>
|
||||
<i class="bi " [ngClass]="customEventCategory.isParticipationEvent ? 'bi-check-lg text-success' : 'bi-x-lg text-danger'"></i>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-3 justify-content-around">
|
||||
<i ngbTooltip="Edit" placement="auto" (click)="onEditCustomEventCategory(customEventCategory)" class="bi custom-edit-icon bi-pencil-fill"></i>
|
||||
<i ngbTooltip="Delete" placement="auto" (click)="onDeleteCustomEventCategory(customEventCategory)" class="bi custom-delete-icon bi-trash3"></i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination Controls -->
|
||||
<div class="d-flex justify-content-between mt-3 flex-column flex-md-row">
|
||||
<pagination-controls class="custom-pagination" [id]="'customEventTable'" [responsive]="true" [autoHide]=" true"
|
||||
(pageChange)="pageChanged($event)"></pagination-controls>
|
||||
|
||||
</div>
|
||||
} @else {
|
||||
<div class="alert alert-secondary text-center mt-5" role="alert">
|
||||
No saved categories
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {
|
||||
CreateCustomEventCategoryModel,
|
||||
CustomEventCategoryModel,
|
||||
UpdateCustomEventCategoryModel
|
||||
} from "../../../models/customEventCategory.model";
|
||||
import {FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import {JwtTokenService} from "../../../services/jwt-token.service";
|
||||
import {CustomEventCategoryService} from "../../../services/custom-event-category.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-event-category',
|
||||
templateUrl: './custom-event-category.component.html',
|
||||
styleUrl: './custom-event-category.component.css'
|
||||
})
|
||||
export class CustomEventCategoryComponent implements OnInit {
|
||||
isCreateCustomEventCategory: any;
|
||||
customEventCategories: CustomEventCategoryModel[] = [];
|
||||
customEventCategoryForm!: FormGroup;
|
||||
isUpdate: boolean = false;
|
||||
private readonly _tokenService: JwtTokenService = inject(JwtTokenService);
|
||||
private readonly _customEventCategoryService: CustomEventCategoryService = inject(CustomEventCategoryService);
|
||||
private readonly _toastr: ToastrService = inject(ToastrService);
|
||||
private allianceId: string = this._tokenService.getAllianceId()!;
|
||||
pageNumber: number = 1;
|
||||
|
||||
get f() {
|
||||
return this.customEventCategoryForm.controls;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.getCustomEventCategories();
|
||||
}
|
||||
|
||||
getCustomEventCategories() {
|
||||
this._customEventCategoryService.getAllianceCustomEventCategories(this.allianceId).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
this.customEventCategories = response;
|
||||
} else {
|
||||
this.customEventCategories = [];
|
||||
}
|
||||
}),
|
||||
error: (error) => {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCreateCategory() {
|
||||
this.createCustomEventCategoryForm(false);
|
||||
}
|
||||
|
||||
createCustomEventCategoryForm(isUpdate: boolean, customEventCategory: CustomEventCategoryModel | null = null) {
|
||||
this.customEventCategoryForm = new FormGroup({
|
||||
id: new FormControl<string>(isUpdate ? customEventCategory!.id : ''),
|
||||
allianceId: new FormControl<string>(this.allianceId),
|
||||
name: new FormControl<string>(isUpdate ? customEventCategory!.name : '', [Validators.required, Validators.maxLength(150)]),
|
||||
isPointsEvent: new FormControl<boolean>(isUpdate ? customEventCategory!.isPointsEvent : false),
|
||||
isParticipationEvent: new FormControl(isUpdate ? customEventCategory!.isParticipationEvent : false),
|
||||
});
|
||||
if (isUpdate) {
|
||||
this.customEventCategoryForm.controls['isPointsEvent'].disable();
|
||||
this.customEventCategoryForm.controls['isParticipationEvent'].disable();
|
||||
}
|
||||
this.isCreateCustomEventCategory = true;
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.isUpdate = false;
|
||||
this.isCreateCustomEventCategory = false;
|
||||
}
|
||||
|
||||
onEditCustomEventCategory(customEventCategory: CustomEventCategoryModel) {
|
||||
this.createCustomEventCategoryForm(true, customEventCategory);
|
||||
}
|
||||
|
||||
onDeleteCustomEventCategory(customEventCategory: CustomEventCategoryModel) {
|
||||
Swal.fire({
|
||||
title: `Delete category: ${customEventCategory.name}`,
|
||||
text: `Do you really want to delete the category? This will also delete all events associated with this category.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Yes, delete it!"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
this._customEventCategoryService.deleteCustomEventCategory(customEventCategory.id).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
Swal.fire({
|
||||
title: "Deleted!",
|
||||
text: "Custom event has been deleted",
|
||||
icon: "success"
|
||||
}).then(_ => this.resetAndGetCustomEventCategories());
|
||||
}
|
||||
}),
|
||||
error: (error: Error) => {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.customEventCategoryForm.invalid) {
|
||||
return;
|
||||
}
|
||||
if (this.isUpdate) {
|
||||
this.updateCustomEventCategory();
|
||||
return;
|
||||
}
|
||||
const customEventCategory: CreateCustomEventCategoryModel = this.customEventCategoryForm.value as CreateCustomEventCategoryModel;
|
||||
this._customEventCategoryService.createCustomEventCategory(customEventCategory).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
this._toastr.success('Successfully created!', 'Successfully');
|
||||
this.onCancel();
|
||||
this.resetAndGetCustomEventCategories();
|
||||
}
|
||||
}),
|
||||
error: ((error: any) => {
|
||||
console.error(error);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pageChanged(event: number) {
|
||||
this.pageNumber = event;
|
||||
}
|
||||
|
||||
updateCustomEventCategory() {
|
||||
const customEventCategory: UpdateCustomEventCategoryModel = this.customEventCategoryForm.value as UpdateCustomEventCategoryModel;
|
||||
this._customEventCategoryService.updateCustomEvent(customEventCategory.id, customEventCategory).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
this._toastr.success('Successfully updated!', 'Successfully');
|
||||
this.onCancel();
|
||||
this.resetAndGetCustomEventCategories();
|
||||
}
|
||||
}),
|
||||
error: ((error: any) => {
|
||||
console.error(error);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resetAndGetCustomEventCategories() {
|
||||
this.pageNumber = 1;
|
||||
this.getCustomEventCategories();
|
||||
}
|
||||
}
|
||||
@ -48,7 +48,7 @@
|
||||
<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>
|
||||
Achieved Points: <span class="text-primary">{{player.achievedPoints | number}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (customEventDetail.isParticipationEvent) {
|
||||
|
||||
@ -0,0 +1,176 @@
|
||||
@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'].untouched),
|
||||
'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'].untouched)) {
|
||||
<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'].untouched),
|
||||
'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'].untouched)) {
|
||||
<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'].untouched || 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'].untouched || 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>
|
||||
<!-- Category-->
|
||||
<div class="form-floating mb-3" (change)="onCategoryChange()">
|
||||
<select class="form-select" id="customEventCategoryId" formControlName="customEventCategoryId">
|
||||
<option [ngValue]="null">No Category</option>
|
||||
@for (customEventCategory of customEventCategories; track customEventCategory.id) {
|
||||
<option [ngValue]="customEventCategory.id">{{customEventCategory.name}}</option>
|
||||
}
|
||||
</select>
|
||||
<label for="customEventCategoryId">Category</label>
|
||||
</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">Category</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 | paginate: { id: 'customEventTable', itemsPerPage: pageSize, totalItems: totalRecord, currentPage: pageNumber}; track customEvent.id) {
|
||||
<tr>
|
||||
<td>{{customEvent.eventDate | date: 'dd.MM.yyyy'}}</td>
|
||||
<td>{{customEvent.name}}</td>
|
||||
<td>{{customEvent.categoryName ?? ' - '}}</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 ngbTooltip="Show details" placement="auto" (click)="onGoToCustomEventDetail(customEvent)" class="bi custom-info-icon bi-info-circle-fill"></i>
|
||||
<i ngbTooltip="Edit" placement="auto" (click)="onEditCustomEvent(customEvent)" class="bi custom-edit-icon bi-pencil-fill"></i>
|
||||
<i ngbTooltip="Delete" placement="auto" (click)="onDeleteCustomEvent(customEvent)" class="bi custom-delete-icon bi-trash3"></i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination Controls -->
|
||||
<div class="d-flex justify-content-between mt-3 flex-column flex-md-row">
|
||||
<pagination-controls class="custom-pagination" [id]="'customEventTable'" [responsive]="true" [autoHide]=" true"
|
||||
(pageChange)="pageChanged($event)"></pagination-controls>
|
||||
|
||||
<!-- Showing total results with improved styling -->
|
||||
<div class="align-self-center text-muted mt-2 mt-md-0">
|
||||
<small>
|
||||
Showing
|
||||
<strong>{{ (pageNumber - 1) * pageSize + 1 }} - {{ pageNumber * pageSize > totalRecord ? totalRecord : pageNumber * pageSize }}</strong>
|
||||
of <strong>{{ totalRecord }}</strong> results
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="alert alert-secondary text-center mt-5" role="alert">
|
||||
No saved custom events
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,312 @@
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {CreateCustomEventModel, CustomEventDetailModel, CustomEventModel} from "../../../models/customEvent.model";
|
||||
import {FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import {CustomEventParticipantModel} from "../../../models/customEventParticipant.model";
|
||||
import {CustomEventCategoryModel} from "../../../models/customEventCategory.model";
|
||||
import {JwtTokenService} from "../../../services/jwt-token.service";
|
||||
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {CustomEventService} from "../../../services/custom-event.service";
|
||||
import {CustomEventParticipantService} from "../../../services/custom-event-participant.service";
|
||||
import {CustomEventCategoryService} from "../../../services/custom-event-category.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {
|
||||
CustomEventParticipantsModelComponent
|
||||
} from "../../../modals/custom-event-participants-model/custom-event-participants-model.component";
|
||||
import Swal from "sweetalert2";
|
||||
import {forkJoin, Observable} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-event-events',
|
||||
templateUrl: './custom-event-events.component.html',
|
||||
styleUrl: './custom-event-events.component.css'
|
||||
})
|
||||
export class CustomEventEventsComponent 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[] = [];
|
||||
customEventCategories: CustomEventCategoryModel[] = [];
|
||||
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 _customEventCategoryService: CustomEventCategoryService = inject(CustomEventCategoryService);
|
||||
private readonly _router: Router = inject(Router);
|
||||
private readonly _toastr: ToastrService = inject(ToastrService);
|
||||
private allianceId: string = this._tokenService.getAllianceId()!;
|
||||
|
||||
public totalRecord: number = 0;
|
||||
public pageNumber: number = 1;
|
||||
public pageSize: number = 10
|
||||
|
||||
get f() {
|
||||
return this.customEventForm.controls;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.getCustomEvents();
|
||||
this.getCustomEventCategories();
|
||||
}
|
||||
|
||||
getCustomEvents() {
|
||||
this.customEvents = [];
|
||||
this._customEventService.getAllianceCustomEvents(this.allianceId, this.pageNumber, this.pageSize).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
this.customEvents = response.data;
|
||||
this.totalRecord = response.totalRecords;
|
||||
}
|
||||
}),
|
||||
error: (error) => {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCustomEventCategories() {
|
||||
this._customEventCategoryService.getAllianceCustomEventCategories(this.allianceId).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
this.customEventCategories = response;
|
||||
} else {
|
||||
this.customEventCategories = [];
|
||||
}
|
||||
}),
|
||||
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),
|
||||
customEventCategoryId: new FormControl<string | null>(null),
|
||||
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;
|
||||
if (isUpdate) {
|
||||
this.customEventForm.controls['customEventCategoryId'].disable();
|
||||
}
|
||||
}
|
||||
|
||||
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.getRawValue() 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.resetAndGetCustomEvents();
|
||||
}),
|
||||
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.resetAndGetCustomEvents());
|
||||
}
|
||||
}),
|
||||
error: (error: Error) => {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateCustomEvent() {
|
||||
const participantsToUpdate: any[] = [];
|
||||
const customEvent: CustomEventModel = this.customEventForm.getRawValue() 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.resetAndGetCustomEvents();
|
||||
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.resetAndGetCustomEvents();
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pageChanged(event: number) {
|
||||
this.pageNumber = event;
|
||||
this.getCustomEvents();
|
||||
}
|
||||
|
||||
resetAndGetCustomEvents() {
|
||||
this.pageNumber = 1;
|
||||
this.getCustomEvents();
|
||||
}
|
||||
|
||||
onCategoryChange() {
|
||||
const categoryId = this.customEventForm.get('customEventCategoryId')?.value;
|
||||
if (categoryId !== null) {
|
||||
const category = this.customEventCategories.find(e => e.id === categoryId);
|
||||
this.customEventForm.controls['isPointsEvent'].setValue(category?.isPointsEvent);
|
||||
this.customEventForm.controls['isPointsEvent'].disable();
|
||||
this.customEventForm.controls['isParticipationEvent'].setValue(category?.isParticipationEvent);
|
||||
this.customEventForm.controls['isParticipationEvent'].disable();
|
||||
} else {
|
||||
this.customEventForm.controls['isPointsEvent'].setValue(false);
|
||||
this.customEventForm.controls['isParticipationEvent'].setValue(false);
|
||||
this.customEventForm.controls['isPointsEvent'].enable();
|
||||
this.customEventForm.controls['isParticipationEvent'].enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,278 @@
|
||||
<h3 class="text-center mb-4">🏆 Leaderboard</h3>
|
||||
<!-- Info-Toggle Button -->
|
||||
<div class="d-flex justify-content-center mb-3">
|
||||
<button class="btn btn-info" type="button" (click)="toggleInfo()">
|
||||
{{ showInfo ? 'Hide' : 'Show' }} How the Leaderboard is Generated
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (showInfo) {
|
||||
<div class="alert alert-light border p-3">
|
||||
<h5>📊 How the Event Leaderboard is Calculated</h5>
|
||||
<ul #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" ngbNav>
|
||||
<li [ngbNavItem]="1">
|
||||
<button ngbNavLink>Point event</button>
|
||||
<ng-template ngbNavContent>
|
||||
<p>
|
||||
The leaderboard for the <strong>Points Event</strong> is based purely on the total points
|
||||
(<code>AchievedPoints</code>) manually entered per event. There is no participation tracking or bonus logic applied.
|
||||
</p>
|
||||
|
||||
<h6>🔹 Step 1: Enter Points Per Event</h6>
|
||||
<p>
|
||||
For each event, administrators enter the exact number of points a player has earned.
|
||||
These values are saved and linked to the respective player.
|
||||
</p>
|
||||
|
||||
<h6>🔹 Step 2: Total Points Are Calculated</h6>
|
||||
<p>
|
||||
The system adds up all points a player has earned across all events. This sum determines their leaderboard position.
|
||||
No participation status, score bonus, or placement logic is applied.
|
||||
</p>
|
||||
|
||||
<p><strong>Formula:</strong></p>
|
||||
<code>Total Points = Sum of AchievedPoints from all events</code>
|
||||
|
||||
<h5>📌 Example Calculation</h5>
|
||||
<ul>
|
||||
<li><strong>Event 1:</strong> 1,200,000 points</li>
|
||||
<li><strong>Event 2:</strong> 950,000 points</li>
|
||||
<li><strong>Event 3:</strong> 2,300,000 points</li>
|
||||
</ul>
|
||||
<p><strong>Total Points:</strong> <code>1,200,000 + 950,000 + 2,300,000 = 4,450,000</code></p>
|
||||
|
||||
<h5>🔍 How the Leaderboard Works</h5>
|
||||
<p>
|
||||
Players are ranked based on their total accumulated points across all events.
|
||||
The player with the most points is ranked first.
|
||||
</p>
|
||||
<ul>
|
||||
<li>There is no weighting or adjustment applied.</li>
|
||||
<li>Only point totals matter – no bonus logic is used.</li>
|
||||
<li>All players with at least one entered score are shown in the leaderboard.</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="2">
|
||||
<button ngbNavLink>Participation event</button>
|
||||
<ng-template ngbNavContent>
|
||||
<p>
|
||||
The leaderboard for the <strong>Participation Event</strong> is based solely on whether a player took part in each event.
|
||||
No scores or placements are used. The more events a player attends, the higher their ranking.
|
||||
</p>
|
||||
|
||||
<h6>🔹 Step 1: Mark Participation per Event</h6>
|
||||
<p>
|
||||
For each event, administrators simply mark whether a player participated (<code>Participated = true</code>) or not.
|
||||
No further input (like scores or ranks) is required.
|
||||
</p>
|
||||
|
||||
<h6>🔹 Step 2: Total Participations Are Counted</h6>
|
||||
<p>
|
||||
The system counts the number of events each player has participated in. This count becomes the player’s total score.
|
||||
</p>
|
||||
|
||||
<p><strong>Formula:</strong></p>
|
||||
<code>Total Participations = Number of Events where Participated = true</code>
|
||||
|
||||
<h5>📌 Example Calculation</h5>
|
||||
<ul>
|
||||
<li><strong>Event 1:</strong> Participated ✅</li>
|
||||
<li><strong>Event 2:</strong> Not Participated ❌</li>
|
||||
<li><strong>Event 3:</strong> Participated ✅</li>
|
||||
<li><strong>Event 4:</strong> Participated ✅</li>
|
||||
</ul>
|
||||
<p><strong>Total Participations:</strong> <code>3</code></p>
|
||||
|
||||
<h5>🔍 How the Leaderboard Works</h5>
|
||||
<p>
|
||||
Players are ranked by the number of events they have participated in.
|
||||
The player with the highest number of participations is listed first.
|
||||
</p>
|
||||
<ul>
|
||||
<li>There are no scores or ranking bonuses.</li>
|
||||
<li>Only actual participation is counted.</li>
|
||||
<li>All players who are part of the event list are shown, even with 0 participations.</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="3">
|
||||
<button ngbNavLink>Point and participation</button>
|
||||
<ng-template ngbNavContent>
|
||||
<p>
|
||||
The leaderboard ranks players based on their <strong>participation</strong> and <strong>performance</strong> in events.
|
||||
The calculation rewards consistency and effort, while still recognizing high individual scores.
|
||||
</p>
|
||||
|
||||
<h6>🔹 Step 1: Participation Bonus (High Weighting)</h6>
|
||||
<p>
|
||||
Every time a player is marked as <code>Participated = true</code>, they receive a fixed bonus of <strong>10 points</strong>.
|
||||
This ensures that active players gain a strong foundation in the leaderboard.
|
||||
</p>
|
||||
<p><strong>Formula:</strong></p>
|
||||
<code>Total Participation Bonus = Number of Participations × 10</code>
|
||||
|
||||
<h6>🔹 Step 2: Placement Bonus (Moderate Weighting)</h6>
|
||||
<p>
|
||||
Players are ranked within each event based on <code>AchievedPoints</code>. The top 100 ranked participants receive bonus points:
|
||||
</p>
|
||||
<ul>
|
||||
<li>1st place = 100 points</li>
|
||||
<li>2nd place = 99 points</li>
|
||||
<li>...</li>
|
||||
<li>100th place = 1 point</li>
|
||||
</ul>
|
||||
<p>
|
||||
This value is normalized (divided by 100) and then multiplied by <strong>2.0</strong>.
|
||||
</p>
|
||||
<p><strong>Formula (per event):</strong></p>
|
||||
<code>Placement Bonus = (Rank Points / 100) × 2</code>
|
||||
|
||||
<h6>🔹 Step 3: Score Bonus (Lower Weighting)</h6>
|
||||
<p>
|
||||
Each player's raw score (<code>AchievedPoints</code>) is converted into a bonus using a logarithmic scale:
|
||||
<code>log10(Score + 1)</code>. This prevents extremely high scores from dominating the leaderboard.
|
||||
</p>
|
||||
<p><strong>Formula (per event):</strong></p>
|
||||
<code>Score Bonus = log10(AchievedPoints + 1) × 1.0</code>
|
||||
|
||||
<h5>📌 Example Calculation</h5>
|
||||
<ul>
|
||||
<li><strong>Events Participated:</strong> 3</li>
|
||||
<li><strong>Event 1:</strong> 1st place, 1,200,000 points</li>
|
||||
<li><strong>Event 2:</strong> 8th place, 950,000 points</li>
|
||||
<li><strong>Event 3:</strong> Participated, 10,000 points</li>
|
||||
</ul>
|
||||
<p>Final Calculation:</p>
|
||||
<ul>
|
||||
<li>Participation Bonus: <code>3 × 10 = 30</code></li>
|
||||
<li>Placement Bonus: <code>(1.00 + 0.93 + 0.21) × 2 = 4.28</code></li>
|
||||
<li>Score Bonus: <code>log10(1,200,001) + log10(950,001) + log10(10,001) ≈ 6.08 + 5.98 + 4.00 = 16.06</code></li>
|
||||
<li><strong>Total Points: 30 + 4.28 + 16.06 = 50.34</strong></li>
|
||||
</ul>
|
||||
|
||||
<h5>🔍 How the Leaderboard Works</h5>
|
||||
<p>
|
||||
Players are ranked based on their total points accumulated across all events. The system encourages players to join events regularly and perform well.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Consistent participation</strong> is the best way to climb the leaderboard.</li>
|
||||
<li><strong>High scores</strong> matter, but do not outweigh multiple solid participations.</li>
|
||||
<li><strong>All players assigned to events</strong> appear on the leaderboard, even with no participations.</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
</div>
|
||||
|
||||
}
|
||||
<div class="form-floating mb-3" (change)="onCategoryChange($event)">
|
||||
<select class="form-select" id="customEventCategoryId">
|
||||
<option [value]="null">Select a category</option>
|
||||
@for (customEventCategory of customEventCategories; track customEventCategory.id) {
|
||||
<option [value]="customEventCategory.id">{{customEventCategory.name}}</option>
|
||||
}
|
||||
</select>
|
||||
<label for="customEventCategoryId">Category</label>
|
||||
</div>
|
||||
|
||||
@if (selectedCustomEventCategory) {
|
||||
<div class="d-flex justify-content-between mt-3">
|
||||
<p>Category: <span class="text-primary">{{selectedCustomEventCategory.name}}</span></p>
|
||||
<p>
|
||||
Point event:
|
||||
<i class="bi" [ngClass]="selectedCustomEventCategory.isPointsEvent ? 'bi-check-lg text-success' : 'bi-x-lg text-danger'"></i>
|
||||
</p>
|
||||
<p>
|
||||
Participation event:
|
||||
<i class="bi" [ngClass]="selectedCustomEventCategory.isParticipationEvent ? 'bi-check-lg text-success' : 'bi-x-lg text-danger'"></i>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
@if (pointEventLeaderboard) {
|
||||
@if (pointEventLeaderboard.length > 0) {
|
||||
<div class="table-responsive mt-5">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Player</th>
|
||||
<th scope="col">Total points</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (leader of pointEventLeaderboard | paginate: { id: 'pointEventTable', itemsPerPage: 8, currentPage: pointPageNumber}; track leader.playerName) {
|
||||
<tr>
|
||||
<td>{{leader.playerName}}</td>
|
||||
<td>{{leader.points |number}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<pagination-controls class="custom-pagination" [id]="'pointEventTable'" [responsive]="true" [autoHide]=" true"
|
||||
(pageChange)="pointPageChanged($event)"></pagination-controls>
|
||||
} @else {
|
||||
<p>No events in category: {{selectedCustomEventCategory?.name}}</p>
|
||||
}
|
||||
}
|
||||
|
||||
@if (participationEventLeaderboard) {
|
||||
@if (participationEventLeaderboard.length > 0) {
|
||||
<div class="table-responsive mt-5">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Player</th>
|
||||
<th scope="col">Total participations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (leader of participationEventLeaderboard | paginate: { id: 'participationEventTable', itemsPerPage: 8, currentPage: participationPageNumber}; track leader.playerName) {
|
||||
<tr>
|
||||
<td>{{leader.playerName}}</td>
|
||||
<td>{{leader.participations}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<pagination-controls class="custom-pagination" [id]="'participationEventTable'" [responsive]="true" [autoHide]=" true"
|
||||
(pageChange)="participationPageChanged($event)"></pagination-controls>
|
||||
} @else {
|
||||
<p>No events in category: {{selectedCustomEventCategory?.name}}</p>
|
||||
}
|
||||
}
|
||||
|
||||
@if (pointAndParticipationEventLeaderboard) {
|
||||
@if (pointAndParticipationEventLeaderboard.length > 0) {
|
||||
<div class="table-responsive mt-5">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Player</th>
|
||||
<th scope="col">Score points</th>
|
||||
<th scope="col">Total participations</th>
|
||||
<th scope="col">Total points</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (leader of pointAndParticipationEventLeaderboard | paginate: { id: 'pointAndParticipationEventTable', itemsPerPage: 8, currentPage: pointAndParticipationPageNumber}; track leader.playerName) {
|
||||
<tr>
|
||||
<td>{{leader.playerName}}</td>
|
||||
<td>{{leader.totalPoints}}</td>
|
||||
<td>{{leader.participations}}</td>
|
||||
<td>{{leader.points | number}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<pagination-controls class="custom-pagination" [id]="'pointAndParticipationEventTable'" [responsive]="true" [autoHide]=" true"
|
||||
(pageChange)="pointAndParticipationPageChanged($event)"></pagination-controls>
|
||||
} @else {
|
||||
<p>No events in category: {{selectedCustomEventCategory?.name}}</p>
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {CustomEventCategoryModel} from "../../../models/customEventCategory.model";
|
||||
import {CustomEventCategoryService} from "../../../services/custom-event-category.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {JwtTokenService} from "../../../services/jwt-token.service";
|
||||
import {CustomEventLeaderboardService} from "../../../services/custom-event-leaderboard.service";
|
||||
import {
|
||||
LeaderboardParticipationEventModel, LeaderboardPointAndParticipationEventModel,
|
||||
LeaderboardPointEventModel
|
||||
} from "../../../models/customEventLeaderboard.model";
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-event-leaderboard',
|
||||
templateUrl: './custom-event-leaderboard.component.html',
|
||||
styleUrl: './custom-event-leaderboard.component.css'
|
||||
})
|
||||
export class CustomEventLeaderboardComponent implements OnInit {
|
||||
|
||||
private readonly _customEventCategoryService: CustomEventCategoryService = inject(CustomEventCategoryService);
|
||||
private readonly _toastr: ToastrService = inject(ToastrService);
|
||||
private readonly _tokenService: JwtTokenService = inject(JwtTokenService);
|
||||
private readonly _customLeaderboardService: CustomEventLeaderboardService = inject(CustomEventLeaderboardService);
|
||||
|
||||
private allianceId: string = this._tokenService.getAllianceId()!;
|
||||
|
||||
customEventCategories: CustomEventCategoryModel[] = [];
|
||||
selectedCustomEventCategory: CustomEventCategoryModel | undefined;
|
||||
pointEventLeaderboard: LeaderboardPointEventModel[] | undefined;
|
||||
participationEventLeaderboard: LeaderboardParticipationEventModel[] | undefined;
|
||||
pointAndParticipationEventLeaderboard: LeaderboardPointAndParticipationEventModel[] | undefined;
|
||||
pointPageNumber: number = 1;
|
||||
participationPageNumber: number = 1;
|
||||
pointAndParticipationPageNumber: number = 1;
|
||||
showInfo: boolean = false;
|
||||
activeTab: number = 1;
|
||||
|
||||
ngOnInit() {
|
||||
this.getCustomEventCategories();
|
||||
}
|
||||
|
||||
getCustomEventCategories() {
|
||||
this._customEventCategoryService.getAllianceCustomEventCategories(this.allianceId).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
this.customEventCategories = response;
|
||||
} else {
|
||||
this.customEventCategories = [];
|
||||
}
|
||||
}),
|
||||
error: (error) => {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCategoryChange(event: any) {
|
||||
this.pointEventLeaderboard = undefined;
|
||||
this.participationEventLeaderboard = undefined;
|
||||
this.pointAndParticipationEventLeaderboard = undefined;
|
||||
const categoryId = event.target.value;
|
||||
if (categoryId !== 'null') {
|
||||
this.selectedCustomEventCategory = this.customEventCategories.find(e => e.id === categoryId);
|
||||
if (this.selectedCustomEventCategory!.isPointsEvent && this.selectedCustomEventCategory!.isParticipationEvent) {
|
||||
this.getPointAndParticipationEventLeaderboard(this.selectedCustomEventCategory!.id);
|
||||
return;
|
||||
}
|
||||
if (this.selectedCustomEventCategory!.isParticipationEvent) {
|
||||
this.getParticipationEventLeaderboard(this.selectedCustomEventCategory!.id);
|
||||
return;
|
||||
}
|
||||
if (this.selectedCustomEventCategory!.isPointsEvent) {
|
||||
this.getPointEventLeaderboard(this.selectedCustomEventCategory!.id);
|
||||
}
|
||||
} else {
|
||||
this.selectedCustomEventCategory = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getPointEventLeaderboard(customEventCategoryId: string) {
|
||||
this._customLeaderboardService.getPointEvent(customEventCategoryId).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
this.pointEventLeaderboard = response;
|
||||
} else {
|
||||
this.pointEventLeaderboard = [];
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getParticipationEventLeaderboard(customEventCategoryId: string) {
|
||||
this._customLeaderboardService.getParticipationEvent(customEventCategoryId).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
this.participationEventLeaderboard = response;
|
||||
} else {
|
||||
this.participationEventLeaderboard = [];
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getPointAndParticipationEventLeaderboard(customEventCategoryId: string) {
|
||||
this._customLeaderboardService.getPointAndParticipationEvent(customEventCategoryId).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
this.pointAndParticipationEventLeaderboard = response;
|
||||
} else {
|
||||
this.pointAndParticipationEventLeaderboard = [];
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pointPageChanged(event: number) {
|
||||
this.pointPageNumber = event;
|
||||
}
|
||||
|
||||
participationPageChanged(event: number) {
|
||||
this.participationPageNumber = event;
|
||||
}
|
||||
|
||||
pointAndParticipationPageChanged(event: number) {
|
||||
this.pointAndParticipationPageNumber = event;
|
||||
}
|
||||
|
||||
toggleInfo() {
|
||||
this.showInfo = !this.showInfo;
|
||||
}
|
||||
}
|
||||
@ -1,169 +1,25 @@
|
||||
<div class="container mt-3 pb-5">
|
||||
<h2 class="text-center">Custom Event</h2>
|
||||
|
||||
@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 | paginate: { id: 'customEventTable', itemsPerPage: pageSize, totalItems: totalRecord, currentPage: pageNumber}; 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 ngbTooltip="Show details" placement="auto" (click)="onGoToCustomEventDetail(customEvent)" class="bi custom-info-icon bi-info-circle-fill"></i>
|
||||
<i ngbTooltip="Edit" placement="auto" (click)="onEditCustomEvent(customEvent)" class="bi custom-edit-icon bi-pencil-fill"></i>
|
||||
<i ngbTooltip="Delete" placement="auto" (click)="onDeleteCustomEvent(customEvent)" class="bi custom-delete-icon bi-trash3"></i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination Controls -->
|
||||
<div class="d-flex justify-content-between mt-3 flex-column flex-md-row">
|
||||
<pagination-controls class="custom-pagination" [id]="'customEventTable'" [responsive]="true" [autoHide]=" true"
|
||||
(pageChange)="pageChanged($event)"></pagination-controls>
|
||||
|
||||
<!-- Showing total results with improved styling -->
|
||||
<div class="align-self-center text-muted mt-2 mt-md-0">
|
||||
<small>
|
||||
Showing
|
||||
<strong>{{ (pageNumber - 1) * pageSize + 1 }} - {{ pageNumber * pageSize > totalRecord ? totalRecord : pageNumber * pageSize }}</strong>
|
||||
of <strong>{{ totalRecord }}</strong> results
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="alert alert-secondary text-center mt-5" role="alert">
|
||||
No saved custom events
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<ul #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" ngbNav>
|
||||
<li [ngbNavItem]="1">
|
||||
<button ngbNavLink>Events</button>
|
||||
<ng-template ngbNavContent>
|
||||
<app-custom-event-events></app-custom-event-events>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="2">
|
||||
<button ngbNavLink>Category</button>
|
||||
<ng-template ngbNavContent>
|
||||
<app-custom-event-category></app-custom-event-category>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="3">
|
||||
<button ngbNavLink>Leaderboard</button>
|
||||
<ng-template ngbNavContent>
|
||||
<app-custom-event-leaderboard></app-custom-event-leaderboard>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
</div>
|
||||
|
||||
@ -1,272 +1,12 @@
|
||||
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";
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-event',
|
||||
templateUrl: './custom-event.component.html',
|
||||
styleUrl: './custom-event.component.css'
|
||||
})
|
||||
export class CustomEventComponent implements OnInit {
|
||||
export class CustomEventComponent {
|
||||
|
||||
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()!;
|
||||
|
||||
public totalRecord: number = 0;
|
||||
public pageNumber: number = 1;
|
||||
public pageSize: number = 10
|
||||
|
||||
get f() {
|
||||
return this.customEventForm.controls;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.getCustomEvents();
|
||||
}
|
||||
|
||||
getCustomEvents() {
|
||||
this.customEvents = [];
|
||||
this._customEventService.getAllianceCustomEvents(this.allianceId, this.pageNumber, this.pageSize).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
this.customEvents = response.data;
|
||||
this.totalRecord = response.totalRecords;
|
||||
}
|
||||
}),
|
||||
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.resetAndGetCustomEvents();
|
||||
}),
|
||||
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.resetAndGetCustomEvents());
|
||||
}
|
||||
}),
|
||||
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.resetAndGetCustomEvents();
|
||||
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.resetAndGetCustomEvents();
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pageChanged(event: number) {
|
||||
this.pageNumber = event;
|
||||
this.getCustomEvents();
|
||||
}
|
||||
|
||||
resetAndGetCustomEvents() {
|
||||
this.pageNumber = 1;
|
||||
this.getCustomEvents();
|
||||
}
|
||||
public activeTab: number = 1;
|
||||
}
|
||||
|
||||
38
Ui/src/app/services/custom-event-category.service.ts
Normal file
38
Ui/src/app/services/custom-event-category.service.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {environment} from "../../environments/environment";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {Observable} from "rxjs";
|
||||
import {
|
||||
CreateCustomEventCategoryModel,
|
||||
CustomEventCategoryModel,
|
||||
UpdateCustomEventCategoryModel
|
||||
} from "../models/customEventCategory.model";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CustomEventCategoryService {
|
||||
|
||||
private readonly _serviceUrl = environment.apiBaseUrl + 'customEventCategories/';
|
||||
private readonly _httpClient: HttpClient = inject(HttpClient);
|
||||
|
||||
getAllianceCustomEventCategories(allianceId: string): Observable<CustomEventCategoryModel[]> {
|
||||
return this._httpClient.get<CustomEventCategoryModel[]>(this._serviceUrl + 'Alliance/' + allianceId);
|
||||
}
|
||||
|
||||
getCustomEventCategory(customEventCategoryId: string): Observable<CustomEventCategoryModel> {
|
||||
return this._httpClient.get<CustomEventCategoryModel>(this._serviceUrl + customEventCategoryId);
|
||||
}
|
||||
|
||||
createCustomEventCategory(customEventCategory: CreateCustomEventCategoryModel): Observable<CustomEventCategoryModel> {
|
||||
return this._httpClient.post<CustomEventCategoryModel>(this._serviceUrl, customEventCategory);
|
||||
}
|
||||
|
||||
updateCustomEvent(customEventCategoryId: string, customEvent: UpdateCustomEventCategoryModel): Observable<CustomEventCategoryModel> {
|
||||
return this._httpClient.put<CustomEventCategoryModel>(this._serviceUrl + customEventCategoryId, customEvent);
|
||||
}
|
||||
|
||||
deleteCustomEventCategory(customEventCategoryId: string): Observable<boolean> {
|
||||
return this._httpClient.delete<boolean>(this._serviceUrl + customEventCategoryId );
|
||||
}
|
||||
}
|
||||
31
Ui/src/app/services/custom-event-leaderboard.service.ts
Normal file
31
Ui/src/app/services/custom-event-leaderboard.service.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {environment} from "../../environments/environment";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {Observable} from "rxjs";
|
||||
import {
|
||||
LeaderboardParticipationEventModel,
|
||||
LeaderboardPointAndParticipationEventModel,
|
||||
LeaderboardPointEventModel
|
||||
} from "../models/customEventLeaderboard.model";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CustomEventLeaderboardService {
|
||||
|
||||
private readonly _serviceUrl = environment.apiBaseUrl + 'customEventLeaderboards/';
|
||||
private readonly _httpClient: HttpClient = inject(HttpClient);
|
||||
|
||||
getPointEvent(customEventCategoryId: string): Observable<LeaderboardPointEventModel[]> {
|
||||
return this._httpClient.get<LeaderboardPointEventModel[]>(this._serviceUrl + 'point/' + customEventCategoryId);
|
||||
}
|
||||
|
||||
getParticipationEvent(customEventCategoryId: string): Observable<LeaderboardParticipationEventModel[]> {
|
||||
return this._httpClient.get<LeaderboardParticipationEventModel[]>(this._serviceUrl + 'participation/' + customEventCategoryId);
|
||||
}
|
||||
|
||||
getPointAndParticipationEvent(customEventCategoryId: string): Observable<LeaderboardPointAndParticipationEventModel[]> {
|
||||
return this._httpClient.get<LeaderboardPointAndParticipationEventModel[]>(this._serviceUrl + 'point-and-participation/' + customEventCategoryId);
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user