mirror of
https://github.com/TomasiDeveloping/PlayerManagement.git
synced 2026-04-16 09:12:20 +00:00
implement players import from excel file
This commit is contained in:
parent
9931a9a3c2
commit
4efec5642e
@ -1,4 +1,5 @@
|
||||
using Application.DataTransferObjects.Player;
|
||||
using Application.DataTransferObjects.ExcelImport;
|
||||
using Application.DataTransferObjects.Player;
|
||||
using Application.Errors;
|
||||
using Application.Interfaces;
|
||||
using Asp.Versioning;
|
||||
@ -12,7 +13,7 @@ namespace Api.Controllers.v1
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
public class PlayersController(IPlayerRepository playerRepository, IClaimTypeService claimTypeService, ILogger<PlayersController> logger) : ControllerBase
|
||||
public class PlayersController(IPlayerRepository playerRepository, IClaimTypeService claimTypeService, IExcelService excelService, ILogger<PlayersController> logger) : ControllerBase
|
||||
{
|
||||
[HttpGet("{playerId:guid}")]
|
||||
public async Task<ActionResult<PlayerDto>> GetPlayer(Guid playerId, CancellationToken cancellationToken)
|
||||
@ -159,6 +160,39 @@ namespace Api.Controllers.v1
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("ExcelImport")]
|
||||
public async Task<ActionResult<ExcelImportResponse>> ImportPlayersFromExcel(
|
||||
[FromForm] ExcelImportRequest excelImportRequest, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (excelImportRequest.ExcelFile.Length == 0)
|
||||
{
|
||||
return BadRequest(new Error("", "No excel file upload"));
|
||||
}
|
||||
|
||||
var allowedExtensions = new[] { ".xlsx", ".xls" };
|
||||
var fileExtension = Path.GetExtension(excelImportRequest.ExcelFile.FileName);
|
||||
|
||||
if (!allowedExtensions.Contains(fileExtension))
|
||||
{
|
||||
return BadRequest(new Error("", "No supported excel file"));
|
||||
}
|
||||
|
||||
var excelImportResult = await excelService.ImportPlayersFromExcelAsync(excelImportRequest,
|
||||
claimTypeService.GetFullName(User), cancellationToken);
|
||||
|
||||
return excelImportResult.IsFailure
|
||||
? BadRequest(excelImportResult.Error)
|
||||
: Ok(excelImportResult.Value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, e.Message);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{playerId:guid}")]
|
||||
public async Task<ActionResult<PlayerDto>> UpdatePlayer(Guid playerId, UpdatePlayerDto updatePlayerDto, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="ExcelDataReader" Version="3.7.0" />
|
||||
<PackageReference Include="ExcelDataReader.DataSet" Version="3.7.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ public static class ApplicationDependencyInjection
|
||||
services.AddScoped<IZombieSiegeRepository, ZombieSiegeRepository>();
|
||||
services.AddScoped<IZombieSiegeParticipantRepository, ZombieSiegeParticipantRepository>();
|
||||
services.AddScoped<IVsDuelLeagueRepository, VsDuelLeagueRepository>();
|
||||
services.AddScoped<IExcelService, ExcelService>();
|
||||
|
||||
|
||||
services.AddTransient<IJwtService, JwtService>();
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Application.DataTransferObjects.ExcelImport;
|
||||
|
||||
public class ExcelImportRequest
|
||||
{
|
||||
public required IFormFile ExcelFile { get; set; }
|
||||
|
||||
public Guid AllianceId { get; set; }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Application.DataTransferObjects.ExcelImport;
|
||||
|
||||
public class ExcelImportResponse
|
||||
{
|
||||
public int AddSum { get; set; }
|
||||
|
||||
public int SkipSum { get; set; }
|
||||
}
|
||||
9
Application/Interfaces/IExcelService.cs
Normal file
9
Application/Interfaces/IExcelService.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Application.Classes;
|
||||
using Application.DataTransferObjects.ExcelImport;
|
||||
|
||||
namespace Application.Interfaces;
|
||||
|
||||
public interface IExcelService
|
||||
{
|
||||
Task<Result<ExcelImportResponse>> ImportPlayersFromExcelAsync(ExcelImportRequest excelImportRequest, string createdBy, CancellationToken cancellationToken);
|
||||
}
|
||||
124
Application/Services/ExcelService.cs
Normal file
124
Application/Services/ExcelService.cs
Normal file
@ -0,0 +1,124 @@
|
||||
using Application.Classes;
|
||||
using Application.Interfaces;
|
||||
using ExcelDataReader;
|
||||
using System.Data;
|
||||
using Application.DataTransferObjects.ExcelImport;
|
||||
using Database;
|
||||
using Database.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Application.Services;
|
||||
|
||||
public class ExcelService(ApplicationContext context, ILogger<ExcelService> logger) : IExcelService
|
||||
{
|
||||
public async Task<Result<ExcelImportResponse>> ImportPlayersFromExcelAsync(ExcelImportRequest excelImportRequest, string createdBy, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
|
||||
|
||||
await using var stream = excelImportRequest.ExcelFile.OpenReadStream();
|
||||
using var reader = ExcelReaderFactory.CreateReader(stream);
|
||||
|
||||
var result = reader.AsDataSet();
|
||||
|
||||
var dataTable = result.Tables[0];
|
||||
|
||||
if (dataTable.Rows.Count == 0)
|
||||
{
|
||||
return Result.Failure<ExcelImportResponse>(new Error("","Excel has no data"));
|
||||
}
|
||||
|
||||
var columnMapping = GetColumnMappingFromHeader(dataTable.Rows[0]);
|
||||
|
||||
var requiredHeader = new[] { "Name", "Rank", "Level" };
|
||||
|
||||
foreach (var header in requiredHeader)
|
||||
{
|
||||
if (!columnMapping.ContainsKey(header))
|
||||
{
|
||||
return Result.Failure<ExcelImportResponse>(new Error("", $"Required header “{header}” is missing in the file."));
|
||||
}
|
||||
}
|
||||
|
||||
var validRanks = await context.Ranks.AsNoTracking().ToListAsync(cancellationToken);
|
||||
var alliancePlayers = await context.Players.Where(p => p.AllianceId == excelImportRequest.AllianceId).Select(p => p.PlayerName.Trim().ToLower())
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var players = new List<Player>();
|
||||
|
||||
var addCounter = 0;
|
||||
var skipCounter = 0;
|
||||
|
||||
for (var row = 1; row < dataTable.Rows.Count; row++)
|
||||
{
|
||||
var dataRow = dataTable.Rows[row];
|
||||
|
||||
var name = dataRow[columnMapping["Name"]].ToString()?.Trim();
|
||||
var level = int.TryParse(dataRow[columnMapping["Level"]].ToString(), out var parsedLevel) ? parsedLevel : 0;
|
||||
var rankName = dataRow[columnMapping["Rank"]].ToString()?.Trim() ?? "R1";
|
||||
|
||||
var rank = validRanks.FirstOrDefault(r => string.Equals(r.Name, rankName, StringComparison.CurrentCultureIgnoreCase));
|
||||
|
||||
if (rank is null)
|
||||
{
|
||||
return Result.Failure<ExcelImportResponse>(new Error("", $"Invalid rank “{rankName}” in row {row + 1}."));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return Result.Failure<ExcelImportResponse>(new Error("", $"Player name is row {row + 1} is empty"));
|
||||
}
|
||||
|
||||
if (alliancePlayers.Contains(name.ToLower().Trim()))
|
||||
{
|
||||
skipCounter++;
|
||||
continue;
|
||||
}
|
||||
|
||||
players.Add(new Player()
|
||||
{
|
||||
CreatedBy = createdBy,
|
||||
PlayerName = name,
|
||||
Level = level,
|
||||
Id = Guid.CreateVersion7(),
|
||||
RankId = rank.Id,
|
||||
CreatedOn = DateTime.Now,
|
||||
AllianceId = excelImportRequest.AllianceId,
|
||||
IsDismissed = false
|
||||
});
|
||||
addCounter++;
|
||||
}
|
||||
|
||||
await context.Players.AddRangeAsync(players, cancellationToken);
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success(new ExcelImportResponse()
|
||||
{
|
||||
AddSum = addCounter,
|
||||
SkipSum = skipCounter
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, e.Message);
|
||||
return Result.Failure<ExcelImportResponse>(new Error("", ""));
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> GetColumnMappingFromHeader(DataRow headerRow)
|
||||
{
|
||||
var columnMapping = new Dictionary<string, int>();
|
||||
|
||||
for (var col = 0; col < headerRow.Table.Columns.Count; col++)
|
||||
{
|
||||
var header = headerRow[col].ToString()?.Trim();
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
columnMapping.TryAdd(header, col);
|
||||
}
|
||||
}
|
||||
|
||||
return columnMapping;
|
||||
}
|
||||
}
|
||||
@ -54,6 +54,7 @@ import { CustomEventParticipantsModelComponent } from './modals/custom-event-par
|
||||
import { CustomEventDetailComponent } from './pages/custom-event/custom-event-detail/custom-event-detail.component';
|
||||
import { DismissPlayerComponent } from './pages/dismiss-player/dismiss-player.component';
|
||||
import { PlayerDismissInformationModalComponent } from './modals/player-dismiss-information-modal/player-dismiss-information-modal.component';
|
||||
import { PlayerExcelImportModalComponent } from './modals/player-excel-import-modal/player-excel-import-modal.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -98,7 +99,8 @@ import { PlayerDismissInformationModalComponent } from './modals/player-dismiss-
|
||||
CustomEventParticipantsModelComponent,
|
||||
CustomEventDetailComponent,
|
||||
DismissPlayerComponent,
|
||||
PlayerDismissInformationModalComponent
|
||||
PlayerDismissInformationModalComponent,
|
||||
PlayerExcelImportModalComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
/* app.component.css */
|
||||
.upload-area {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 30px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.upload-area i {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.upload-area p {
|
||||
font-size: 1.2rem;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #198754 !important;
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
<div class="modal-header" xmlns="http://www.w3.org/1999/html">
|
||||
<h4 class="modal-title">Import players from Excel</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="activeModal.dismiss()"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Description -->
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<p>
|
||||
This feature allows you to upload an Excel file to add players. Please follow the guidelines below:
|
||||
</p>
|
||||
<ul>
|
||||
<li>The Excel file must have <strong>headers in the first row</strong> (the first row of your Excel sheet).</li>
|
||||
<li>Players with already existing names will <strong>not be imported</strong>.</li>
|
||||
<li>The file must include the following headers (order does not matter):
|
||||
<ul>
|
||||
<li><strong>Name</strong>: The player's name</li>
|
||||
<li><strong>Rank</strong>: The player's rank (R1 - R5)</li>
|
||||
<li><strong>Level</strong>: The player's headquarters level</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Additional headers in the file will be <strong>ignored</strong>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Upload Area -->
|
||||
<div class="upload-area text-center p-5 border border-primary rounded">
|
||||
@if (!selectedFile) {
|
||||
<div>
|
||||
<i class="bi bi-file-earmark-excel-fill display-3 text-primary"></i>
|
||||
<p class="mt-3">Choose an Excel file (.xlsx or .xls)</p>
|
||||
<input
|
||||
type="file"
|
||||
class="form-control d-none"
|
||||
id="fileInput"
|
||||
(change)="onFileSelected($event)"
|
||||
accept=".xlsx, .xls"
|
||||
#fileInput
|
||||
/>
|
||||
<button class="btn btn-primary mt-3" (click)="fileInput.click()">Select File</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div>
|
||||
<i class="bi bi-file-earmark-check-fill display-3 text-success"></i>
|
||||
<p class="mt-3"><strong>{{ selectedFile.name }}</strong></p>
|
||||
<button class="btn btn-danger mt-3" (click)="removeFile()">Remove File</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-warning" (click)="activeModal.dismiss()">Close</button>
|
||||
<button (click)="onFileUpload()" [disabled]="!selectedFile" type="submit" class=" btn btn-success">Upload</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import Swal from "sweetalert2";
|
||||
import {JwtTokenService} from "../../services/jwt-token.service";
|
||||
import {PlayerService} from "../../services/player.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-player-excel-import-modal',
|
||||
templateUrl: './player-excel-import-modal.component.html',
|
||||
styleUrl: './player-excel-import-modal.component.css'
|
||||
})
|
||||
export class PlayerExcelImportModalComponent {
|
||||
|
||||
private readonly _toastr: ToastrService = inject(ToastrService);
|
||||
private readonly _tokenService: JwtTokenService = inject(JwtTokenService);
|
||||
private readonly _playerService: PlayerService = inject(PlayerService);
|
||||
|
||||
public activeModal: NgbActiveModal = inject(NgbActiveModal);
|
||||
selectedFile: File | null = null;
|
||||
|
||||
onFileUpload() {
|
||||
if (!this.selectedFile) {
|
||||
Swal.fire('No file uploaded', 'Please select a file first', 'error').then();
|
||||
return;
|
||||
}
|
||||
this._playerService.uploadPlayerFromExcel(this._tokenService.getAllianceId()!, this.selectedFile).subscribe({
|
||||
next: ((response) => {
|
||||
if (response) {
|
||||
Swal.fire('Uploaded successfully', `${response.addSum} player added, ${response.skipSum} player skipped`, 'success').then(() =>
|
||||
this.activeModal.close(response.addSum > 0),);
|
||||
}
|
||||
}),
|
||||
error: (error) => {
|
||||
console.log(error);
|
||||
if (error.error.name) {
|
||||
Swal.fire('Error', error.error.name, 'error').then(() => {this.selectedFile = null;});
|
||||
} else {
|
||||
this._toastr.error('Could not import players from Excel', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onFileSelected(event: any): void {
|
||||
const file = event.target.files[0];
|
||||
if (
|
||||
file &&
|
||||
(file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel')
|
||||
) {
|
||||
this.selectedFile = file;
|
||||
} else {
|
||||
Swal.fire('Incorrect file type', 'Please upload only .xlsx or .xls files.', 'warning').then();
|
||||
this.selectedFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
removeFile() {
|
||||
this.selectedFile = null;
|
||||
}
|
||||
}
|
||||
4
Ui/src/app/models/excelImportResponse.model.ts
Normal file
4
Ui/src/app/models/excelImportResponse.model.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface ExcelImportResponseModel {
|
||||
addSum: number;
|
||||
skipSum: number;
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
<div class="container mt-3 pb-5">
|
||||
<h2 class="text-center">Alliance Players</h2>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button (click)="onAddNewPlayer()" class="btn btn-info mb-3">Add new Player</button>
|
||||
<button (click)="onAddNewPlayer()" class="btn btn-info mb-3"> <i class="bi bi-person-plus"></i> Add new Player</button>
|
||||
<button (click)="onAddImportFromExcel()" class="btn btn-info mb-3 ms-3"><i class="bi bi-filetype-xlsx"></i> Import from Excel</button>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3"><span class="badge text-bg-dark">Members: {{players.length}}/100</span></h5>
|
||||
|
||||
@ -11,6 +11,9 @@ import {PlayerEditModalComponent} from "../../modals/player-edit-modal/player-ed
|
||||
import Swal from 'sweetalert2'
|
||||
import {Router} from "@angular/router";
|
||||
import {JwtTokenService} from "../../services/jwt-token.service";
|
||||
import {
|
||||
PlayerExcelImportModalComponent
|
||||
} from "../../modals/player-excel-import-modal/player-excel-import-modal.component";
|
||||
|
||||
|
||||
|
||||
@ -205,4 +208,17 @@ export class PlayerComponent implements OnInit {
|
||||
}
|
||||
this.filteredPlayers = [...this.activePlayers];
|
||||
}
|
||||
|
||||
onAddImportFromExcel() {
|
||||
const modalRef = this._modalService.open(PlayerExcelImportModalComponent,
|
||||
{animation: true, backdrop: 'static', centered: true, size: 'lg'});
|
||||
modalRef.closed.subscribe({
|
||||
next: ((response: boolean) => {
|
||||
if (response) {
|
||||
this.filter.patchValue('');
|
||||
this.getPlayers(this.allianceId!);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
PlayerModel,
|
||||
UpdatePlayerModel
|
||||
} from "../models/player.model";
|
||||
import {ExcelImportResponseModel} from "../models/excelImportResponse.model";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -56,6 +57,14 @@ export class PlayerService {
|
||||
return this._httpClient.post<PlayerModel>(this._serviceUrl, player);
|
||||
}
|
||||
|
||||
public uploadPlayerFromExcel(allianceId: string, excelFile: File): Observable<ExcelImportResponseModel> {
|
||||
const formData = new FormData();
|
||||
formData.append('excelFile', excelFile);
|
||||
formData.append('allianceId', allianceId);
|
||||
|
||||
return this._httpClient.post<ExcelImportResponseModel>(this._serviceUrl + 'ExcelImport', formData);
|
||||
}
|
||||
|
||||
public deletePlayer(playerId: string): Observable<boolean> {
|
||||
return this._httpClient.delete<boolean>(this._serviceUrl + playerId);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user