This commit is contained in:
Tomasi - Developing 2025-04-23 16:03:41 +02:00
parent c48d8d8211
commit 711fb4adaa
49 changed files with 4842 additions and 439 deletions

View 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");
}
}
}
}

View 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");
}
}
}
}

View File

@ -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>();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
namespace Application.DataTransferObjects.CustomEventLeaderboard;
public class LeaderboardParticipationEventDto
{
public required string PlayerName { get; set; }
public int Participations { get; set; }
}

View File

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

View File

@ -0,0 +1,8 @@
namespace Application.DataTransferObjects.CustomEventLeaderboard;
public class LeaderboardPointEventDto
{
public required string PlayerName { get; set; }
public long Points { get; set; }
}

View 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");
}

View 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);
}

View 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>();
}
}

View File

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

View 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);
}
}
}

View 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);
}
}

View File

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

View File

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

View 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);
}
}

View File

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

View File

@ -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; } = [];
}

View File

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

View 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; } = [];
}

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View 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);
}
}
}

View File

@ -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");

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 );
}
}

View 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);
}
}