v. 0.11.0

This commit is contained in:
Tomasi - Developing 2025-06-19 11:02:57 +02:00
parent fa3a0ec218
commit bfbb030cb2
48 changed files with 6983 additions and 57 deletions

View File

@ -0,0 +1,37 @@
using Application.DataTransferObjects.SquadType;
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 SquadTypesController(ISquadTypeRepository squadTypeRepository, ILogger<SquadTypesController> logger)
: ControllerBase
{
[HttpGet]
public async Task<ActionResult<List<SquadTypeDto>>> GetSquadTypes(CancellationToken cancellationToken)
{
try
{
var squadTypesResult = await squadTypeRepository.GetSquadTypesAsync(cancellationToken);
if (squadTypesResult.IsFailure) return BadRequest(squadTypesResult.Error);
return squadTypesResult.Value.Count > 0
? Ok(squadTypesResult.Value)
: NoContent();
}
catch (Exception e)
{
logger.LogError(e, "{ErrorMessage}", e.Message);
return Problem(
detail: $"Failed to process {nameof(GetSquadTypes)}",
statusCode: StatusCodes.Status500InternalServerError,
title: "Internal server error");
}
}
}
}

View File

@ -0,0 +1,106 @@
using Application.DataTransferObjects.Squad;
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 SquadsController(ISquadRepository squadRepository, ILogger<SquadsController> logger) : ControllerBase
{
[HttpGet("player/{playerId:guid}")]
public async Task<ActionResult<List<SquadDto>>> GetPlayerSquads(Guid playerId, CancellationToken cancellationToken)
{
try
{
var playerSquadsResult = await squadRepository.GetPlayerSquadsAsync(playerId, cancellationToken);
if (playerSquadsResult.IsFailure) return BadRequest(playerSquadsResult.Error);
return playerSquadsResult.Value.Count > 0
? Ok(playerSquadsResult.Value)
: NoContent();
}
catch (Exception e)
{
logger.LogError(e, "{ErrorMessage}", e.Message);
return Problem(
detail: $"Failed to process {nameof(GetPlayerSquads)}",
statusCode: StatusCodes.Status500InternalServerError,
title: "Internal server error");
}
}
[HttpPost]
public async Task<ActionResult<SquadDto>> CreateSquad(CreateSquadDto createSquadDto,
CancellationToken cancellationToken)
{
try
{
if (!ModelState.IsValid) return UnprocessableEntity(ModelState);
var createSquadResult = await squadRepository.CreateSquadAsync(createSquadDto, cancellationToken);
return createSquadResult.IsSuccess
? Ok(createSquadResult.Value)
: BadRequest(createSquadResult.Error);
}
catch (Exception e)
{
logger.LogError(e, "{ErrorMessage}", e.Message);
return Problem(
detail: $"Failed to process {nameof(GetPlayerSquads)}",
statusCode: StatusCodes.Status500InternalServerError,
title: "Internal server error");
}
}
[HttpPut("{squadId:guid}")]
public async Task<ActionResult<SquadDto>> UpdateSquad(Guid squadId, UpdateSquadDto updateSquadDto, CancellationToken cancellationToken)
{
try
{
if (!ModelState.IsValid) return UnprocessableEntity(ModelState);
var updateSquadResult = await squadRepository.UpdateSquadAsync(updateSquadDto, cancellationToken);
return updateSquadResult.IsSuccess
? Ok(updateSquadResult.Value)
: BadRequest(updateSquadResult.Error);
}
catch (Exception e)
{
logger.LogError(e, "{ErrorMessage}", e.Message);
return Problem(
detail: $"Failed to process {nameof(UpdateSquad)}",
statusCode: StatusCodes.Status500InternalServerError,
title: "Internal server error");
}
}
[HttpDelete("{squadId:guid}")]
public async Task<ActionResult<bool>> DeleteSquad(Guid squadId, CancellationToken cancellationToken)
{
try
{
var deleteSquadResult = await squadRepository.DeleteSquadAsync(squadId, cancellationToken);
return deleteSquadResult.IsSuccess
? Ok(deleteSquadResult.Value)
: BadRequest(deleteSquadResult.Error);
}
catch (Exception e)
{
logger.LogError(e, "{ErrorMessage}", e.Message);
return Problem(
detail: $"Failed to process {nameof(DeleteSquad)}",
statusCode: StatusCodes.Status500InternalServerError,
title: "Internal server error");
}
}
}
}

View File

@ -1,23 +0,0 @@
using Api.Helpers;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
namespace Api.Controllers.v1
{
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1.0")]
public class ValuesController : ControllerBase
{
[AllowApiKey]
[HttpGet]
public async Task<IActionResult> Test([FromQuery] Guid allianceId, [FromQuery] string? key)
{
return Ok(new
{
AllianceId = allianceId,
Key = key
});
}
}
}

View File

@ -37,6 +37,8 @@ public static class ApplicationDependencyInjection
services.AddScoped<ICustomEventCategoryRepository, CustomEventCategoryRepository>();
services.AddScoped<ICustomEventLeaderBoardRepository, CustomEventLeaderboardRepository>();
services.AddScoped<IStatRepository, StatRepository>();
services.AddScoped<ISquadTypeRepository, SquadTypeRepository>();
services.AddScoped<ISquadRepository, SquadRepository>();
services.AddTransient<IJwtService, JwtService>();

View File

@ -0,0 +1,12 @@
namespace Application.DataTransferObjects.Squad;
public class CreateSquadDto
{
public Guid SquadTypeId { get; set; }
public Guid PlayerId { get; set; }
public decimal Power { get; set; }
public int Position { get; set; }
}

View File

@ -0,0 +1,19 @@
namespace Application.DataTransferObjects.Squad;
public class SquadDto
{
public Guid Id { get; set; }
public Guid SquadTypeId { get; set; }
public Guid PlayerId { get; set; }
public required string TypeName { get; set; }
public decimal Power { get; set; }
public int Position { get; set; }
public DateTime LastUpdateAt { get; set; }
}

View File

@ -0,0 +1,14 @@
namespace Application.DataTransferObjects.Squad;
public class UpdateSquadDto
{
public Guid Id { get; set; }
public Guid SquadTypeId { get; set; }
public Guid PlayerId { get; set; }
public decimal Power { get; set; }
public int Position { get; set; }
}

View File

@ -0,0 +1,9 @@
namespace Application.DataTransferObjects.SquadType;
public class SquadTypeDto
{
public Guid Id { get; set; }
public required string TypeName { get; set; }
}

View File

@ -0,0 +1,9 @@
namespace Application.Errors;
public class SquadErrors
{
public static readonly Error NotFound = new("Error.Squad.NotFound",
"The quad with the specified identifier was not found");
public static readonly Error IdConflict = new("Error.Note.IdConflict", "There is a conflict with the id's");
}

View File

@ -0,0 +1,15 @@
using Application.Classes;
using Application.DataTransferObjects.Squad;
namespace Application.Interfaces;
public interface ISquadRepository
{
Task<Result<List<SquadDto>>> GetPlayerSquadsAsync(Guid playerId, CancellationToken cancellationToken = default);
Task<Result<SquadDto>> CreateSquadAsync(CreateSquadDto createSquadDto, CancellationToken cancellationToken = default);
Task<Result<SquadDto>> UpdateSquadAsync(UpdateSquadDto updateSquadDto, CancellationToken cancellationToken = default);
Task<Result<bool>> DeleteSquadAsync(Guid squadId, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,9 @@
using Application.Classes;
using Application.DataTransferObjects.SquadType;
namespace Application.Interfaces;
public interface ISquadTypeRepository
{
Task<Result<List<SquadTypeDto>>> GetSquadTypesAsync(CancellationToken cancellationToken);
}

View File

@ -0,0 +1,21 @@
using Application.DataTransferObjects.Squad;
using AutoMapper;
using Database.Entities;
namespace Application.Profiles;
public class SquadProfile : Profile
{
public SquadProfile()
{
CreateMap<Squad, SquadDto>()
.ForMember(des => des.TypeName, opt => opt.MapFrom(src => src.SquadType.TypeName));
CreateMap<CreateSquadDto, Squad>()
.ForMember(dest => dest.LastUpdateAt, opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => Guid.CreateVersion7()));
CreateMap<UpdateSquadDto, Squad>()
.ForMember(des => des.LastUpdateAt, opt => opt.MapFrom(src => DateTime.UtcNow));
}
}

View File

@ -0,0 +1,13 @@
using Application.DataTransferObjects.SquadType;
using AutoMapper;
using Database.Entities;
namespace Application.Profiles;
public class SquadTypeProfile : Profile
{
public SquadTypeProfile()
{
CreateMap<SquadType, SquadTypeDto>();
}
}

View File

@ -0,0 +1,91 @@
using Application.Classes;
using Application.DataTransferObjects.Squad;
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 SquadRepository(ApplicationContext dbContext, IMapper mapper, ILogger<SquadRepository> logger) : ISquadRepository
{
public async Task<Result<List<SquadDto>>> GetPlayerSquadsAsync(Guid playerId, CancellationToken cancellationToken = default)
{
var playerSquads = await dbContext.Squads
.Where(squad => squad.PlayerId == playerId)
.ProjectTo<SquadDto>(mapper.ConfigurationProvider)
.OrderBy(squad => squad.Position)
.AsNoTracking()
.ToListAsync(cancellationToken);
return Result.Success(playerSquads);
}
public async Task<Result<SquadDto>> CreateSquadAsync(CreateSquadDto createSquadDto, CancellationToken cancellationToken = default)
{
try
{
var newSuad = mapper.Map<Squad>(createSquadDto);
await dbContext.Squads.AddAsync(newSuad, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return Result.Success(mapper.Map<SquadDto>(newSuad));
}
catch (Exception e)
{
logger.LogError(e, "{DatabaseException}", e.Message);
return Result.Failure<SquadDto>(GeneralErrors.DatabaseError);
}
}
public async Task<Result<SquadDto>> UpdateSquadAsync(UpdateSquadDto updateSquadDto, CancellationToken cancellationToken = default)
{
try
{
var squadToUpdate = await dbContext.Squads
.FirstOrDefaultAsync(squad => squad.Id == updateSquadDto.Id, cancellationToken);
if (squadToUpdate is null) return Result.Failure<SquadDto>(SquadErrors.NotFound);
mapper.Map(updateSquadDto, squadToUpdate);
await dbContext.SaveChangesAsync(cancellationToken);
return Result.Success(mapper.Map<SquadDto>(squadToUpdate));
}
catch (Exception e)
{
logger.LogError(e, "{DatabaseException}", e.Message);
return Result.Failure<SquadDto>(GeneralErrors.DatabaseError);
}
}
public async Task<Result<bool>> DeleteSquadAsync(Guid squadId, CancellationToken cancellationToken = default)
{
try
{
var squadToDelete = await dbContext.Squads
.FirstOrDefaultAsync(squad => squad.Id == squadId, cancellationToken);
if (squadToDelete is null) return Result.Failure<bool>(SquadErrors.NotFound);
dbContext.Squads.Remove(squadToDelete);
await dbContext.SaveChangesAsync(cancellationToken);
return Result.Success(mapper.Map<bool>(true));
}
catch (Exception e)
{
logger.LogError(e, "{DatabaseException}", e.Message);
return Result.Failure<bool>(GeneralErrors.DatabaseError);
}
}
}

View File

@ -0,0 +1,22 @@
using Application.Classes;
using Application.DataTransferObjects.SquadType;
using Application.Interfaces;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Database;
using Microsoft.EntityFrameworkCore;
namespace Application.Repositories;
public class SquadTypeRepository(ApplicationContext dbContext, IMapper mapper) : ISquadTypeRepository
{
public async Task<Result<List<SquadTypeDto>>> GetSquadTypesAsync(CancellationToken cancellationToken)
{
var squadTypes = await dbContext.SquadTypes
.ProjectTo<SquadTypeDto>(mapper.ConfigurationProvider)
.AsNoTracking()
.ToListAsync(cancellationToken);
return Result.Success(squadTypes);
}
}

View File

@ -46,6 +46,10 @@ public class ApplicationContext(DbContextOptions<ApplicationContext> options) :
public DbSet<CustomEventCategory> CustomEventCategories { get; set; }
public DbSet<Squad> Squads { get; set; }
public DbSet<SquadType> SquadTypes { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);

View File

@ -0,0 +1,28 @@
using Database.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Database.Configurations;
public class SquadConfiguration : IEntityTypeConfiguration<Squad>
{
public void Configure(EntityTypeBuilder<Squad> builder)
{
builder.HasKey(squad => squad.Id);
builder.Property(squad => squad.Id).ValueGeneratedNever();
builder.Property(squad => squad.Power).IsRequired().HasPrecision(18,2);
builder.Property(squad => squad.Position).IsRequired();
builder.Property(squad => squad.LastUpdateAt).IsRequired();
builder.HasOne(squad => squad.SquadType)
.WithMany(squadType => squadType.Squads)
.HasForeignKey(squad => squad.SquadTypeId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(squatType => squatType.Player)
.WithMany(player => player.Squads)
.HasForeignKey(squad => squad.PlayerId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@ -0,0 +1,46 @@
using Database.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Database.Configurations;
public class SquadTypeConfiguration : IEntityTypeConfiguration<SquadType>
{
public void Configure(EntityTypeBuilder<SquadType> builder)
{
builder.HasKey(squadType => squadType.Id);
builder.Property(squadType => squadType.Id).ValueGeneratedNever();
builder.Property(squadType => squadType.TypeName)
.IsRequired()
.HasMaxLength(150);
var squadTypes = new List<SquadType>()
{
new()
{
Id = Guid.CreateVersion7(),
TypeName = "Tanks"
},
new()
{
Id = Guid.CreateVersion7(),
TypeName = "Missile"
},
new()
{
Id = Guid.CreateVersion7(),
TypeName = "Aircraft"
},
new()
{
Id = Guid.CreateVersion7(),
TypeName = "Mixed"
}
};
builder.HasData(squadTypes);
}
}

View File

@ -41,4 +41,6 @@ public class Player : BaseEntity
public ICollection<CustomEventParticipant> CustomEventParticipants { get; set; } = [];
public ICollection<ZombieSiegeParticipant> ZombieSiegeParticipants { get; set; } = [];
public ICollection<Squad> Squads { get; set; } = [];
}

View File

@ -0,0 +1,18 @@
namespace Database.Entities;
public class Squad : BaseEntity
{
public Guid SquadTypeId { get; set; }
public SquadType SquadType { get; set; } = null!;
public decimal Power { get; set; }
public int Position { get; set; }
public Guid PlayerId { get; set; }
public Player Player { get; set; } = null!;
public DateTime LastUpdateAt { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace Database.Entities;
public class SquadType : BaseEntity
{
public required string TypeName { get; set; }
public ICollection<Squad> Squads { get; set; } = [];
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,96 @@
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_Squds : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SquadType",
schema: "dbo",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TypeName = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SquadType", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Squad",
schema: "dbo",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SquadTypeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Power = table.Column<long>(type: "bigint", nullable: false),
PlayerId = table.Column<Guid>(type: "uniqueidentifier", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Squad", x => x.Id);
table.ForeignKey(
name: "FK_Squad_Players_PlayerId",
column: x => x.PlayerId,
principalSchema: "dbo",
principalTable: "Players",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Squad_SquadType_SquadTypeId",
column: x => x.SquadTypeId,
principalSchema: "dbo",
principalTable: "SquadType",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.InsertData(
schema: "dbo",
table: "SquadType",
columns: new[] { "Id", "TypeName" },
values: new object[,]
{
{ new Guid("01977cd8-bb62-7089-85cb-5a48223a6e92"), "Aircraft" },
{ new Guid("01977cd8-bb62-7150-a0f9-5415e46a87e4"), "Mixed" },
{ new Guid("01977cd8-bb62-79aa-9a71-95d57250d723"), "Missile" },
{ new Guid("01977cd8-bb62-7d5b-823e-b77c6121c4f1"), "Tanks" }
});
migrationBuilder.CreateIndex(
name: "IX_Squad_PlayerId",
schema: "dbo",
table: "Squad",
column: "PlayerId");
migrationBuilder.CreateIndex(
name: "IX_Squad_SquadTypeId",
schema: "dbo",
table: "Squad",
column: "SquadTypeId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Squad",
schema: "dbo");
migrationBuilder.DropTable(
name: "SquadType",
schema: "dbo");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,224 @@
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_Squad : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Squad_Players_PlayerId",
schema: "dbo",
table: "Squad");
migrationBuilder.DropForeignKey(
name: "FK_Squad_SquadType_SquadTypeId",
schema: "dbo",
table: "Squad");
migrationBuilder.DropPrimaryKey(
name: "PK_SquadType",
schema: "dbo",
table: "SquadType");
migrationBuilder.DropPrimaryKey(
name: "PK_Squad",
schema: "dbo",
table: "Squad");
migrationBuilder.RenameTable(
name: "SquadType",
schema: "dbo",
newName: "SquadTypes",
newSchema: "dbo");
migrationBuilder.RenameTable(
name: "Squad",
schema: "dbo",
newName: "Squads",
newSchema: "dbo");
migrationBuilder.RenameIndex(
name: "IX_Squad_SquadTypeId",
schema: "dbo",
table: "Squads",
newName: "IX_Squads_SquadTypeId");
migrationBuilder.RenameIndex(
name: "IX_Squad_PlayerId",
schema: "dbo",
table: "Squads",
newName: "IX_Squads_PlayerId");
migrationBuilder.AddColumn<int>(
name: "Position",
schema: "dbo",
table: "Squads",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddPrimaryKey(
name: "PK_SquadTypes",
schema: "dbo",
table: "SquadTypes",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_Squads",
schema: "dbo",
table: "Squads",
column: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Squads_Players_PlayerId",
schema: "dbo",
table: "Squads",
column: "PlayerId",
principalSchema: "dbo",
principalTable: "Players",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Squads_SquadTypes_SquadTypeId",
schema: "dbo",
table: "Squads",
column: "SquadTypeId",
principalSchema: "dbo",
principalTable: "SquadTypes",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Squads_Players_PlayerId",
schema: "dbo",
table: "Squads");
migrationBuilder.DropForeignKey(
name: "FK_Squads_SquadTypes_SquadTypeId",
schema: "dbo",
table: "Squads");
migrationBuilder.DropPrimaryKey(
name: "PK_SquadTypes",
schema: "dbo",
table: "SquadTypes");
migrationBuilder.DropPrimaryKey(
name: "PK_Squads",
schema: "dbo",
table: "Squads");
migrationBuilder.DeleteData(
schema: "dbo",
table: "SquadTypes",
keyColumn: "Id",
keyValue: new Guid("01977d00-1596-7078-b24d-f5abd8baaec1"));
migrationBuilder.DeleteData(
schema: "dbo",
table: "SquadTypes",
keyColumn: "Id",
keyValue: new Guid("01977d00-1596-71a1-bbff-db88a4a59f32"));
migrationBuilder.DeleteData(
schema: "dbo",
table: "SquadTypes",
keyColumn: "Id",
keyValue: new Guid("01977d00-1596-7644-98aa-ff20f05f13bb"));
migrationBuilder.DeleteData(
schema: "dbo",
table: "SquadTypes",
keyColumn: "Id",
keyValue: new Guid("01977d00-1596-7c18-a665-e079870ae3cb"));
migrationBuilder.DropColumn(
name: "Position",
schema: "dbo",
table: "Squads");
migrationBuilder.RenameTable(
name: "SquadTypes",
schema: "dbo",
newName: "SquadType",
newSchema: "dbo");
migrationBuilder.RenameTable(
name: "Squads",
schema: "dbo",
newName: "Squad",
newSchema: "dbo");
migrationBuilder.RenameIndex(
name: "IX_Squads_SquadTypeId",
schema: "dbo",
table: "Squad",
newName: "IX_Squad_SquadTypeId");
migrationBuilder.RenameIndex(
name: "IX_Squads_PlayerId",
schema: "dbo",
table: "Squad",
newName: "IX_Squad_PlayerId");
migrationBuilder.AddPrimaryKey(
name: "PK_SquadType",
schema: "dbo",
table: "SquadType",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_Squad",
schema: "dbo",
table: "Squad",
column: "Id");
migrationBuilder.InsertData(
schema: "dbo",
table: "SquadType",
columns: new[] { "Id", "TypeName" },
values: new object[,]
{
{ new Guid("01977cd8-bb62-7089-85cb-5a48223a6e92"), "Aircraft" },
{ new Guid("01977cd8-bb62-7150-a0f9-5415e46a87e4"), "Mixed" },
{ new Guid("01977cd8-bb62-79aa-9a71-95d57250d723"), "Missile" },
{ new Guid("01977cd8-bb62-7d5b-823e-b77c6121c4f1"), "Tanks" }
});
migrationBuilder.AddForeignKey(
name: "FK_Squad_Players_PlayerId",
schema: "dbo",
table: "Squad",
column: "PlayerId",
principalSchema: "dbo",
principalTable: "Players",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Squad_SquadType_SquadTypeId",
schema: "dbo",
table: "Squad",
column: "SquadTypeId",
principalSchema: "dbo",
principalTable: "SquadType",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
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_Lastupdate_to_squad : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastUpdateAt",
schema: "dbo",
table: "Squads",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastUpdateAt",
schema: "dbo",
table: "Squads");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace Database.Migrations
{
/// <inheritdoc />
public partial class Change_squad_power_to_decimal : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<decimal>(
name: "Power",
schema: "dbo",
table: "Squads",
type: "decimal(18,2)",
precision: 18,
scale: 2,
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<long>(
name: "Power",
schema: "dbo",
table: "Squads",
type: "bigint",
nullable: false,
oldClrType: typeof(decimal),
oldType: "decimal(18,2)",
oldPrecision: 18,
oldScale: 2);
}
}
}

View File

@ -510,6 +510,73 @@ namespace Database.Migrations
});
});
modelBuilder.Entity("Database.Entities.Squad", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("LastUpdateAt")
.HasColumnType("datetime2");
b.Property<Guid>("PlayerId")
.HasColumnType("uniqueidentifier");
b.Property<int>("Position")
.HasColumnType("int");
b.Property<decimal>("Power")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<Guid>("SquadTypeId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("PlayerId");
b.HasIndex("SquadTypeId");
b.ToTable("Squads", "dbo");
});
modelBuilder.Entity("Database.Entities.SquadType", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("TypeName")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("nvarchar(150)");
b.HasKey("Id");
b.ToTable("SquadTypes", "dbo");
b.HasData(
new
{
Id = new Guid("01978732-b32b-7af9-bf5f-a69585d89eb7"),
TypeName = "Tanks"
},
new
{
Id = new Guid("01978732-b32b-7fe8-9ee5-68b0d0df44f4"),
TypeName = "Missile"
},
new
{
Id = new Guid("01978732-b32b-780e-bef0-6bcf784ee1b4"),
TypeName = "Aircraft"
},
new
{
Id = new Guid("01978732-b32b-79c9-adaa-19ed53157f67"),
TypeName = "Mixed"
});
});
modelBuilder.Entity("Database.Entities.User", b =>
{
b.Property<Guid>("Id")
@ -661,19 +728,19 @@ namespace Database.Migrations
b.HasData(
new
{
Id = new Guid("01972f47-5fb5-7584-a312-e749206f0036"),
Id = new Guid("01978732-b32e-7aa5-8e77-507b8b20977d"),
Code = 1,
Name = "Silver League"
},
new
{
Id = new Guid("01972f47-5fb6-7dec-93aa-b62f0e167fde"),
Id = new Guid("01978732-b32e-76d6-9beb-3fd062efe9ef"),
Code = 2,
Name = "Gold League"
},
new
{
Id = new Guid("01972f47-5fb6-7f9a-8a2d-9f47fa1803e1"),
Id = new Guid("01978732-b32e-7583-9522-90dde6d778b6"),
Code = 3,
Name = "Diamond League"
});
@ -1078,6 +1145,25 @@ namespace Database.Migrations
b.Navigation("Rank");
});
modelBuilder.Entity("Database.Entities.Squad", b =>
{
b.HasOne("Database.Entities.Player", "Player")
.WithMany("Squads")
.HasForeignKey("PlayerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Database.Entities.SquadType", "SquadType")
.WithMany("Squads")
.HasForeignKey("SquadTypeId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Player");
b.Navigation("SquadType");
});
modelBuilder.Entity("Database.Entities.User", b =>
{
b.HasOne("Database.Entities.Alliance", "Alliance")
@ -1260,6 +1346,8 @@ namespace Database.Migrations
b.Navigation("Notes");
b.Navigation("Squads");
b.Navigation("VsDuelParticipants");
b.Navigation("ZombieSiegeParticipants");
@ -1270,6 +1358,11 @@ namespace Database.Migrations
b.Navigation("Players");
});
modelBuilder.Entity("Database.Entities.SquadType", b =>
{
b.Navigation("Squads");
});
modelBuilder.Entity("Database.Entities.VsDuel", b =>
{
b.Navigation("VsDuelParticipants");

View File

@ -21,6 +21,19 @@ This project is currently in the **Beta Phase**.
---
### **[0.11.0]** - *2025-06-19*
#### ✨ Added
- **Squad Management per Player:** You can now add up to **four squads per player**, each with:
- **Power** (in millions)
- **Position**
- **Type**: Tank, Aircraft, Missile, or Mixed
- **Visual Enhancements:** Squad types are displayed with custom icons and total power is summarized at the top of the squad card.
🛠️ **Fixed**
- (N/A)
---
### **[0.10.0]** - *2025-06-02*
#### ✨ Added
- **Team Selection for Desert Storm:** You can now assign **Team A** or **Team B** when creating a Desert Storm entry.

View File

@ -66,6 +66,8 @@ import { CustomEventLeaderboardComponent } from './pages/custom-event/custom-eve
import { CustomEventEventsComponent } from './pages/custom-event/custom-event-events/custom-event-events.component';
import {NgxMaskDirective, NgxMaskPipe, provideNgxMask} from "ngx-mask";
import {CountUpModule} from "ngx-countup";
import { PlayerSquadsComponent } from './pages/player-squads/player-squads.component';
import { SquadEditModalComponent } from './modals/squad-edit-modal/squad-edit-modal.component';
@NgModule({
declarations: [
@ -119,7 +121,9 @@ import {CountUpModule} from "ngx-countup";
ImprintComponent,
CustomEventCategoryComponent,
CustomEventLeaderboardComponent,
CustomEventEventsComponent
CustomEventEventsComponent,
PlayerSquadsComponent,
SquadEditModalComponent
],
imports: [
BrowserModule,

View File

@ -0,0 +1,68 @@
@if (squadForm) {
<div class="modal-header" xmlns="http://www.w3.org/1999/html">
<h4 class="modal-title">{{ isUpdate ? 'Update Squad ' : 'Insert new Squad' }}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="activeModal.dismiss()"></button>
</div>
<div class="modal-body">
<form [formGroup]="squadForm">
<div class="form-floating mb-3 is-invalid">
<select [ngClass]="{
'is-invalid': f['position'].invalid && (f['position'].dirty || !f['position'].untouched),
'is-valid': f['position'].valid}" class="form-select" id="position" formControlName="position">
@for (position of positions; track position) {
<option [ngValue]="position">{{ position }}</option>
}
</select>
<label for="position">Position</label>
@if (f['position'].invalid && (f['position'].dirty || !f['position'].untouched)) {
<div class="invalid-feedback">
@if (f['position'].hasError('required')) {
<p>Position is required</p>
}
</div>
}
</div>
<div class="form-floating mb-3 is-invalid">
<input [ngClass]="{
'is-invalid': f['power'].invalid && (f['power'].dirty || !f['power'].untouched),
'is-valid': f['power'].valid}"
type="number" class="form-control" id="power" placeholder="power" formControlName="power">
<label for="power">Power in millions</label>
@if (f['power'].invalid && (f['power'].dirty || !f['power'].untouched)) {
<div class="invalid-feedback">
@if (f['power'].hasError('required')) {
<p>Power is required</p>
}
</div>
}
</div>
<div class="form-floating mb-3 is-invalid">
<select [ngClass]="{
'is-invalid': f['squadTypeId'].invalid && (f['squadTypeId'].dirty || !f['squadTypeId'].untouched),
'is-valid': f['squadTypeId'].valid}" class="form-select" id="squadTypeId" formControlName="squadTypeId">
@for (type of squadTypes; track type.id) {
<option [ngValue]="type.id">{{ type.typeName }}</option>
}
</select>
<label for="squadTypeId">Squad Type</label>
@if (f['squadTypeId'].invalid && (f['squadTypeId'].dirty || !f['squadTypeId'].untouched)) {
<div class="invalid-feedback">
@if (f['squadTypeId'].hasError('required')) {
<p>Squad Type is required</p>
}
</div>
}
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" (click)="activeModal.dismiss()">Close</button>
<button [disabled]="squadForm.invalid" (click)="onSubmit()" type="submit"
class=" btn btn-success">{{ isUpdate ? 'Update' : 'Insert' }} Squad
</button>
</div>
}

View File

@ -0,0 +1,94 @@
import {Component, inject, Input, OnInit} from '@angular/core';
import {SquadTypeService} from "../../services/squad-type.service";
import {SquadService} from "../../services/squad.service";
import {SquadTypeModel} from "../../models/squadType.model";
import {CreateSquadModel, SquadModel, UpdateSquadModel} from "../../models/squad.model";
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
import {FormControl, FormGroup, Validators} from "@angular/forms";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-squad-edit-modal',
templateUrl: './squad-edit-modal.component.html',
styleUrl: './squad-edit-modal.component.css'
})
export class SquadEditModalComponent implements OnInit {
public activeModal: NgbActiveModal = inject(NgbActiveModal);
@Input({required: true}) playerId!: string;
@Input({required: true}) isUpdate!: boolean;
@Input({required: true}) currentSquad!: SquadModel;
public squadTypes: SquadTypeModel[] = [];
public positions: number[] = [1, 2, 3, 4];
public squadForm!: FormGroup;
private readonly _squadTypeService: SquadTypeService = inject(SquadTypeService);
private readonly _squadService: SquadService = inject(SquadService);
private readonly _toaster: ToastrService = inject(ToastrService);
get f() {
return this.squadForm.controls;
}
ngOnInit() {
this.getSquadTypes();
this.squadForm = new FormGroup({
id: new FormControl<string>(this.currentSquad.id),
playerId: new FormControl<string>(this.currentSquad.playerId),
power: new FormControl<number | null>(this.isUpdate ? this.currentSquad.power : null, [Validators.required]),
position: new FormControl<number | null>(this.isUpdate ? this.currentSquad.position : null, [Validators.required]),
squadTypeId: new FormControl<string>(this.currentSquad.squadTypeId, [Validators.required]),
})
}
getSquadTypes() {
this._squadTypeService.getSquadTypes().subscribe({
next: (response: SquadTypeModel[]) => {
this.squadTypes = response;
},
error: (error: Error) => {
console.error(error);
this._toaster.error('Could not get squad types', 'Get squad types');
}
});
}
onSubmit() {
if (this.squadForm.invalid) {
return;
}
if (this.isUpdate) {
const squad: UpdateSquadModel = this.squadForm.value as UpdateSquadModel;
this.updateSquad(squad);
} else {
const squad: CreateSquadModel = this.squadForm.value as CreateSquadModel;
this.addSquad(squad);
}
}
private updateSquad(squad: UpdateSquadModel) {
this._squadService.updateSquad(squad.id, squad).subscribe({
next: (_: SquadModel) => {
this._toaster.success('Squad updated successfully', 'Squad update');
this.activeModal.close();
},
error: (error: Error) => {
console.error(error);
this._toaster.error('Could not update squad types', 'Squad update');
}
});
}
private addSquad(squad: CreateSquadModel) {
this._squadService.createSquad(squad).subscribe({
next: (_: SquadModel) => {
this._toaster.success('Squad created successfully', 'Squad create');
this.activeModal.close();
},
error: (error: Error) => {
console.error(error);
this._toaster.error('Could not create squad', 'Squad create');
}
})
}
}

View File

@ -0,0 +1,16 @@
export interface SquadModel extends CreateSquadModel{
id: string;
typeName: string;
lastUpdateAt: Date;
}
export interface CreateSquadModel {
squadTypeId: string;
playerId: string;
power: number;
position: number;
}
export interface UpdateSquadModel extends CreateSquadModel {
id: string;
}

View File

@ -0,0 +1,4 @@
export interface SquadTypeModel {
id: string;
typeName: string;
}

View File

@ -5,9 +5,11 @@
</div>
@if (currentPlayer) {
<!-- Player card-->
<div class="card border-info mt-3">
<div class="row mt-3">
<div class="col-md-6 mb-3 d-flex">
<div class="card border-info flex-fill">
<h5 class="card-header border-info text-center">Player Information</h5>
<div class="card-body">
<div class="card-body d-flex flex-column">
<h5 class="card-title">Name: <span class="text-primary">{{ currentPlayer.playerName }}</span></h5>
<p class="card-text">Rank: <span class="text-primary">{{ currentPlayer.rankName }}</span></p>
<p class="card-text">Headquarter: <span class="text-primary">{{ currentPlayer.level }}</span></p>
@ -19,7 +21,7 @@
class="text-primary">{{ currentPlayer.modifiedOn | date: 'dd.MM.yyyy HH:mm' }}</span></p>
<p class="card-text">Modified by: <span class="text-primary">{{currentPlayer.modifiedBy}}</span></p>
}
<div class="d-flex justify-content-between">
<div class="mt-auto d-flex justify-content-between">
<button (click)="openPlayerNotes(currentPlayer)" type="button" class="btn btn-primary position-relative">
Notes
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
@ -36,9 +38,17 @@
</span>
</button>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3 d-flex">
<app-player-squads [playerId]="playerId"></app-player-squads>
</div>
</div>
</div>
<!-- Accordion-->
<div ngbAccordion #accordion="ngbAccordion" class="mt-3 pb-5">
<!-- Marshal guard-->

View File

@ -64,5 +64,4 @@ export class PlayerInformationComponent implements OnInit {
})
});
}
}

View File

@ -0,0 +1,7 @@
.cursor-pointer {
cursor: pointer;
}
:host {
display: block;
width: 100%;
}

View File

@ -0,0 +1,76 @@
<div class="card border-info flex-fill w-100">
<h5 class="card-header border-info text-center">
Squad Power
@if (squads.length > 0) {
<span class="text-success fw-bold">
{{ totalPower | number:'1.2-2' }} million
</span>
}
</h5>
<div class="card-body">
<!-- Add Squad -->
@if (squads.length < 4) {
<div class="d-flex justify-content-end mb-3">
<button (click)="addSquad()" class="btn btn-sm btn-success" title="Add Squad">
<i class="bi bi-plus-lg"></i> Add
</button>
</div>
}
<!-- Squad List -->
@for (squad of squads; track squad.id) {
<div class="mb-3 border-bottom pb-3">
<!-- Title Row -->
<h6 class="mb-2">{{ squad.position }}. Squad ({{ squad.typeName }})</h6>
<!-- Power | Actions | Image -->
<div class="d-flex align-items-center justify-content-between">
<!-- Power -->
<div>
<strong>Power:</strong> {{ squad.power | number:'1.2-2' }}M
</div>
<!-- Edit / Delete Icons -->
<div>
<i
(click)="editSquad(squad)"
class="bi bi-pencil text-primary me-3 cursor-pointer"
title="Edit Squad">
</i>
<i
(click)="deleteSquad(squad)"
class="bi bi-trash text-danger cursor-pointer"
title="Delete Squad">
</i>
</div>
<!-- Image -->
<img
[src]="'assets/images/icons/' + squad.typeName + '.png'"
alt="{{ squad.typeName }}"
style="width: 50px; height: 50px;"
/>
</div>
<!-- Last Update -->
<div class="text-muted">
<small>Last updated: {{ squad.lastUpdateAt | date:'dd.MM.yyyy' }}</small>
</div>
</div>
}
<!-- No Squads -->
@if (!squads?.length) {
<div class="text-muted text-center">
No squads available.
</div>
}
</div>
</div>

View File

@ -0,0 +1,103 @@
import {Component, inject, Input, OnInit} from '@angular/core';
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
import {SquadEditModalComponent} from "../../modals/squad-edit-modal/squad-edit-modal.component";
import {SquadModel} from "../../models/squad.model";
import {SquadService} from "../../services/squad.service";
import Swal from "sweetalert2";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-player-squads',
templateUrl: './player-squads.component.html',
styleUrl: './player-squads.component.css'
})
export class PlayerSquadsComponent implements OnInit {
@Input({required: true}) playerId!: string;
public squads: SquadModel[] = [];
private readonly _modalService: NgbModal = inject(NgbModal);
private readonly _squadService: SquadService = inject(SquadService);
private readonly _toaster: ToastrService = inject(ToastrService);
get totalPower(): number {
return this.squads?.reduce((sum, squad) => sum + squad.power, 0) || 0;
}
ngOnInit(): void {
this.getPlayerSquads();
}
getPlayerSquads(): void {
this._squadService.getPlayerSquads(this.playerId).subscribe({
next: (result) => {
if (result) {
this.squads = result;
}
},
error: err => {
console.log(err);
this._toaster.error('Error getting player squads', 'Getting player squads');
}
})
}
editSquad(squad: SquadModel) {
this.onOpenModal(this.playerId, squad, true);
}
deleteSquad(squad: SquadModel) {
Swal.fire({
title: "Delete Squad ?",
text: `Do you really want to delete the ${squad.typeName} squad`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, delete it!"
}).then((result) => {
if (result.isConfirmed) {
this._squadService.deleteSquad(squad.id).subscribe({
next: ((response) => {
if (response) {
Swal.fire({
title: "Deleted!",
text: "Squad has been deleted",
icon: "success"
}).then(_ => this.getPlayerSquads());
}
}),
error: (error: Error) => {
console.log(error);
}
});
}
});
}
addSquad() {
const squad: SquadModel = {
id: '',
playerId: this.playerId,
squadTypeId: '',
power: 0,
position: 0,
lastUpdateAt: new Date(),
typeName: ''
};
this.onOpenModal(this.playerId, squad, false);
}
onOpenModal(playerId: string, squad: SquadModel, isUpdate: boolean) {
const modalRef = this._modalService.open(SquadEditModalComponent,
{animation: true, backdrop: 'static', centered: true, size: 'lg'});
modalRef.componentInstance.playerId = playerId;
modalRef.componentInstance.currentSquad = squad;
modalRef.componentInstance.isUpdate = isUpdate;
modalRef.closed.subscribe({
next: (() => {
this.getPlayerSquads();
})
});
}
}

View File

@ -0,0 +1,18 @@
import {inject, Injectable} from '@angular/core';
import {environment} from "../../environments/environment";
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";
import {SquadTypeModel} from "../models/squadType.model";
@Injectable({
providedIn: 'root'
})
export class SquadTypeService {
private readonly _serviceUrl = environment.apiBaseUrl + 'SquadTypes/';
private readonly _httpClient: HttpClient = inject(HttpClient);
getSquadTypes(): Observable<SquadTypeModel[]> {
return this._httpClient.get<SquadTypeModel[]>(this._serviceUrl);
}
}

View File

@ -0,0 +1,30 @@
import {inject, Injectable} from '@angular/core';
import {environment} from "../../environments/environment";
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";
import {CreateSquadModel, SquadModel, UpdateSquadModel} from "../models/squad.model";
@Injectable({
providedIn: 'root'
})
export class SquadService {
private readonly _serviceUrl = environment.apiBaseUrl + 'Squads/';
private readonly _httpClient: HttpClient = inject(HttpClient);
getPlayerSquads(playerId: string): Observable<SquadModel[]> {
return this._httpClient.get<SquadModel[]>(this._serviceUrl + 'player/' + playerId)
}
createSquad(createSquad: CreateSquadModel): Observable<SquadModel> {
return this._httpClient.post<SquadModel>(this._serviceUrl, createSquad);
}
updateSquad(squadId: string, updateSquad: UpdateSquadModel): Observable<SquadModel> {
return this._httpClient.put<SquadModel>(this._serviceUrl + squadId, updateSquad);
}
deleteSquad(squadId: string): Observable<boolean> {
return this._httpClient.delete<boolean>(this._serviceUrl + squadId);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB