553 lines
20 KiB
C#
553 lines
20 KiB
C#
using System.Text.Json;
|
|
using Koogle.Domain.Entities;
|
|
using Koogle.Domain.Enums;
|
|
using Koogle.Domain.Interfaces;
|
|
using Koogle.Infrastructure.Identity;
|
|
using KoogleApp.Data;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
namespace Koogle.Infrastructure.Data;
|
|
|
|
/// <summary>
|
|
/// Seeds demo data for the Demo club including user, persons, expenses, and sample days.
|
|
/// </summary>
|
|
public static class DemoSeeder
|
|
{
|
|
private static readonly string[] MemberNames =
|
|
{
|
|
"Hans Maier", "Klaus Schmidt", "Werner Braun", "Dieter Fischer",
|
|
"Juergen Weber", "Heinz Mueller", "Rolf Schneider", "Karl Hoffmann"
|
|
};
|
|
|
|
private static readonly string[] GuestNames = { "Stefan Gast", "Thomas Besucher" };
|
|
|
|
/// <summary>
|
|
/// DTO for deserializing GIF template entries from giftemplates.json.
|
|
/// </summary>
|
|
private record GifTemplateEntry(string Name, string Filename, string ThrowEventType, string? Description);
|
|
|
|
/// <summary>
|
|
/// Seeds demo user, club membership, persons, expenses, triggers and sample days.
|
|
/// </summary>
|
|
public static async Task SeedAsync(IServiceProvider services, IConfiguration config, IHostEnvironment env)
|
|
{
|
|
var demoEnabled = config.GetValue<bool>("Bootstrap:Demo:Enabled");
|
|
if (!demoEnabled) return;
|
|
|
|
var allowSeed = env.IsDevelopment() || config.GetValue<bool>("Bootstrap:EnableSeeding");
|
|
if (!allowSeed) return;
|
|
|
|
using var scope = services.CreateScope();
|
|
|
|
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
|
var appDb = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var triggerRepo = scope.ServiceProvider.GetRequiredService<ITriggerRepository>();
|
|
|
|
var demoEmail = config["Bootstrap:Demo:Email"] ?? "demo@koogle.de";
|
|
var demoPassword = config["Bootstrap:Demo:Password"] ?? "demo123";
|
|
var demoClubName = config["Bootstrap:Demo:ClubName"] ?? "Demo";
|
|
|
|
// 1) Find or create Demo Club (should exist from BootstrapSeeder if DefaultClub.Name == "Demo")
|
|
var club = await appDb.Clubs.FirstOrDefaultAsync(c => c.Name == demoClubName && !c.IsDeleted);
|
|
if (club == null)
|
|
{
|
|
club = new Club { Name = demoClubName, CreatedAt = DateTime.UtcNow };
|
|
appDb.Clubs.Add(club);
|
|
await appDb.SaveChangesAsync();
|
|
}
|
|
|
|
// 2) Create Demo User (NOT SuperAdmin)
|
|
var demoUser = await userManager.FindByEmailAsync(demoEmail);
|
|
if (demoUser == null)
|
|
{
|
|
demoUser = new ApplicationUser
|
|
{
|
|
UserName = demoEmail,
|
|
Email = demoEmail,
|
|
EmailConfirmed = true
|
|
};
|
|
|
|
var result = await userManager.CreateAsync(demoUser, demoPassword);
|
|
if (!result.Succeeded)
|
|
{
|
|
throw new InvalidOperationException("Failed to create Demo user: " +
|
|
string.Join("; ", result.Errors.Select(e => $"{e.Code}:{e.Description}")));
|
|
}
|
|
}
|
|
|
|
// 3) Create UserProfile for Demo user
|
|
var profile = await appDb.UserProfiles.FirstOrDefaultAsync(p => p.IdentityUserId == demoUser.Id);
|
|
if (profile == null)
|
|
{
|
|
profile = new UserProfile
|
|
{
|
|
IdentityUserId = demoUser.Id,
|
|
DisplayName = "Demo Benutzer",
|
|
Locale = "de-DE",
|
|
TimeZone = "Europe/Berlin",
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
appDb.UserProfiles.Add(profile);
|
|
await appDb.SaveChangesAsync();
|
|
}
|
|
|
|
// 4) Add Demo user to Demo club
|
|
var membership = await appDb.UserProfileClubs.FindAsync(profile.Id, club.Id);
|
|
if (membership == null)
|
|
{
|
|
membership = new UserProfileClub
|
|
{
|
|
UserProfileId = profile.Id,
|
|
ClubId = club.Id,
|
|
IsDefault = true,
|
|
AssignedAt = DateTime.UtcNow,
|
|
AssignedById = demoUser.Id
|
|
};
|
|
appDb.UserProfileClubs.Add(membership);
|
|
await appDb.SaveChangesAsync();
|
|
}
|
|
|
|
// 5) Assign ClubAdmin role to Demo user (NOT SuperAdmin!)
|
|
var roleExists = await appDb.UserProfileClubRoleAssignments.AnyAsync(a =>
|
|
a.UserProfileId == profile.Id && a.ClubId == club.Id && a.RoleName == "Admin" && !a.IsDeleted);
|
|
|
|
if (!roleExists)
|
|
{
|
|
appDb.UserProfileClubRoleAssignments.Add(new UserProfileClubRoleAssignment
|
|
{
|
|
UserProfileId = profile.Id,
|
|
ClubId = club.Id,
|
|
RoleId = Guid.Empty,
|
|
RoleName = "Admin",
|
|
AssignedAt = DateTime.UtcNow,
|
|
AssignedById = demoUser.Id,
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
await appDb.SaveChangesAsync();
|
|
}
|
|
|
|
// 6) Seed demo data if not already present
|
|
var hasPersons = await appDb.Persons.AnyAsync(p => p.ClubId == club.Id && !p.IsDeleted);
|
|
if (!hasPersons)
|
|
{
|
|
await SeedDemoDataAsync(appDb, triggerRepo, club.Id, demoUser.Id);
|
|
}
|
|
|
|
// 7) Seed template GIFs if not already present
|
|
var webEnv = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
|
|
var hasGifs = await appDb.ClubGifs.AnyAsync(g => g.ClubId == club.Id && !g.IsDeleted);
|
|
if (!hasGifs)
|
|
{
|
|
await SeedTemplateGifsInternalAsync(appDb, webEnv, club.Id, club.LoginName);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the Demo club to initial state: hard delete all data, then re-seed.
|
|
/// </summary>
|
|
public static async Task ResetDemoClubAsync(IServiceProvider services, Guid clubId)
|
|
{
|
|
using var scope = services.CreateScope();
|
|
var appDb = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var triggerRepo = scope.ServiceProvider.GetRequiredService<ITriggerRepository>();
|
|
var env = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
|
|
|
|
// Find creator ID and club for seeding
|
|
var club = await appDb.Clubs.FindAsync(clubId);
|
|
if (club == null) return;
|
|
|
|
var membership = await appDb.UserProfileClubs.FirstOrDefaultAsync(m => m.ClubId == clubId);
|
|
var creatorId = membership?.AssignedById ?? Guid.Empty;
|
|
|
|
// Hard delete all club data including GIFs
|
|
await DeleteClubGifsAsync(appDb, env, clubId, club.LoginName);
|
|
await HardDeleteClubDataAsync(appDb, clubId);
|
|
|
|
// Re-seed demo data
|
|
await SeedDemoDataAsync(appDb, triggerRepo, clubId, creatorId);
|
|
|
|
// Re-seed template GIFs
|
|
await SeedTemplateGifsInternalAsync(appDb, env, clubId, club.LoginName);
|
|
}
|
|
|
|
private static async Task HardDeleteClubDataAsync(AppDbContext db, Guid clubId)
|
|
{
|
|
// Order matters due to FK constraints - delete children first
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.PersonExpenses WHERE ClubId = {clubId}");
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.PlayerGameStatistics WHERE ClubId = {clubId}");
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.GamePersons WHERE ClubId = {clubId}");
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.Games WHERE ClubId = {clubId}");
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.DayPersons WHERE ClubId = {clubId}");
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.Days WHERE ClubId = {clubId}");
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.ExpenseTriggers WHERE ClubId = {clubId}");
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.Expenses WHERE ClubId = {clubId}");
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.Persons WHERE ClubId = {clubId}");
|
|
}
|
|
|
|
private static async Task SeedDemoDataAsync(AppDbContext db, ITriggerRepository triggerRepo, Guid clubId, Guid creatorId)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
|
|
// 1) Seed Persons (8 Members + 2 Guests)
|
|
var persons = new List<Person>();
|
|
foreach (var name in MemberNames)
|
|
{
|
|
var person = new Person
|
|
{
|
|
Name = name,
|
|
PersonStatus = PersonStatus.Member,
|
|
ClubId = clubId,
|
|
CreatedAt = now,
|
|
CreatedById = creatorId
|
|
};
|
|
db.Persons.Add(person);
|
|
persons.Add(person);
|
|
}
|
|
|
|
foreach (var name in GuestNames)
|
|
{
|
|
var person = new Person
|
|
{
|
|
Name = name,
|
|
PersonStatus = PersonStatus.Guest,
|
|
ClubId = clubId,
|
|
CreatedAt = now,
|
|
CreatedById = creatorId
|
|
};
|
|
db.Persons.Add(person);
|
|
persons.Add(person);
|
|
}
|
|
await db.SaveChangesAsync();
|
|
|
|
// 2) Seed Expenses with Trigger mappings
|
|
var expenseData = new (string Name, decimal Price, ExpenseTriggerType TriggerType, bool IsInverse)[]
|
|
{
|
|
("Gosse", 0.50m, ExpenseTriggerType.Gutter, false),
|
|
("Pudel", 0.30m, ExpenseTriggerType.NoWood, false),
|
|
("Klingel", 0.20m, ExpenseTriggerType.Bell, false),
|
|
("Anwurffehler", 0.40m, ExpenseTriggerType.FirstThrowFail, false),
|
|
("Kranz", 0.50m, ExpenseTriggerType.Circle, true),
|
|
("Alle Neune", 1.00m, ExpenseTriggerType.Strike, true),
|
|
("Gosse Anwurf", 1.00m, ExpenseTriggerType.FullGutter, false),
|
|
("Ausgeschieden", 2.00m, ExpenseTriggerType.Eliminated, false),
|
|
("Abwesenheit", 5.00m, ExpenseTriggerType.Absent, false),
|
|
("Strafpunkt", 0.10m, ExpenseTriggerType.ExpensePoint, false)
|
|
};
|
|
|
|
var expenses = new List<Expense>();
|
|
foreach (var (name, price, triggerType, isInverse) in expenseData)
|
|
{
|
|
var expense = new Expense
|
|
{
|
|
ClubId = clubId,
|
|
Name = name,
|
|
Description = $"Strafe fuer {name}",
|
|
Price = price,
|
|
IsOneClick = true,
|
|
IsInverse = isInverse,
|
|
IsVariable = false,
|
|
ExpenseType = ExpenseType.Monetary,
|
|
CreatedAt = now,
|
|
CreatedById = creatorId
|
|
};
|
|
db.Expenses.Add(expense);
|
|
expenses.Add(expense);
|
|
}
|
|
await db.SaveChangesAsync();
|
|
|
|
// 3) Create ExpenseTrigger mappings
|
|
for (int i = 0; i < expenses.Count; i++)
|
|
{
|
|
var expense = expenses[i];
|
|
var triggerType = expenseData[i].TriggerType;
|
|
var trigger = await triggerRepo.GetByTriggerTypeAsync(triggerType);
|
|
|
|
if (trigger != null)
|
|
{
|
|
var expenseTrigger = new ExpenseTrigger
|
|
{
|
|
ClubId = clubId,
|
|
ExpenseId = expense.Id,
|
|
TriggerId = trigger.Id,
|
|
AssignedAt = now,
|
|
AssignedById = creatorId
|
|
};
|
|
db.ExpenseTriggers.Add(expenseTrigger);
|
|
}
|
|
}
|
|
await db.SaveChangesAsync();
|
|
|
|
// 4) Seed Sample Days with Games
|
|
await SeedSampleDaysAsync(db, clubId, creatorId, persons, expenses, now);
|
|
}
|
|
|
|
private static async Task SeedSampleDaysAsync(
|
|
AppDbContext db,
|
|
Guid clubId,
|
|
Guid creatorId,
|
|
List<Person> persons,
|
|
List<Expense> expenses,
|
|
DateTime now)
|
|
{
|
|
var members = persons.Where(p => p.PersonStatus == PersonStatus.Member).ToList();
|
|
var gosseExpense = expenses.First(e => e.Name == "Gosse");
|
|
var pudelExpense = expenses.First(e => e.Name == "Pudel");
|
|
var klingelExpense = expenses.First(e => e.Name == "Klingel");
|
|
|
|
// Day 1: 3 participants, 1 game, some expenses
|
|
var day1 = new Day
|
|
{
|
|
PostDate = now.AddDays(-14),
|
|
Status = DayStatus.Closed,
|
|
ClubId = clubId,
|
|
CreatedAt = now,
|
|
CreatedById = creatorId
|
|
};
|
|
db.Days.Add(day1);
|
|
await db.SaveChangesAsync();
|
|
|
|
var day1Participants = members.Take(3).ToList();
|
|
foreach (var p in day1Participants)
|
|
{
|
|
db.Set<DayPerson>().Add(new DayPerson { DayId = day1.Id, ClubId = clubId, PersonId = p.Id });
|
|
}
|
|
|
|
var game1 = new Game
|
|
{
|
|
DayId = day1.Id,
|
|
ClubId = clubId,
|
|
GameType = "Training",
|
|
Status = GameStatus.Completed,
|
|
StartedAt = day1.PostDate,
|
|
CompletedAt = day1.PostDate.AddHours(2),
|
|
CreatedAt = now,
|
|
CreatedById = creatorId
|
|
};
|
|
db.Games.Add(game1);
|
|
await db.SaveChangesAsync();
|
|
|
|
foreach (var p in day1Participants)
|
|
{
|
|
db.Set<GamePerson>().Add(new GamePerson { GameId = game1.Id, ClubId = clubId, PersonId = p.Id });
|
|
}
|
|
await db.SaveChangesAsync();
|
|
|
|
// Day 2: 5 participants, 2 games, diverse expenses
|
|
var day2 = new Day
|
|
{
|
|
PostDate = now.AddDays(-7),
|
|
Status = DayStatus.Closed,
|
|
ClubId = clubId,
|
|
CreatedAt = now,
|
|
CreatedById = creatorId
|
|
};
|
|
db.Days.Add(day2);
|
|
await db.SaveChangesAsync();
|
|
|
|
var day2Participants = members.Take(5).ToList();
|
|
foreach (var p in day2Participants)
|
|
{
|
|
db.Set<DayPerson>().Add(new DayPerson { DayId = day2.Id, ClubId = clubId, PersonId = p.Id });
|
|
}
|
|
|
|
var game2a = new Game
|
|
{
|
|
DayId = day2.Id,
|
|
ClubId = clubId,
|
|
GameType = "Training",
|
|
Status = GameStatus.Completed,
|
|
StartedAt = day2.PostDate,
|
|
CompletedAt = day2.PostDate.AddHours(1),
|
|
CreatedAt = now,
|
|
CreatedById = creatorId
|
|
};
|
|
var game2b = new Game
|
|
{
|
|
DayId = day2.Id,
|
|
ClubId = clubId,
|
|
GameType = "Totenkiste",
|
|
Status = GameStatus.Completed,
|
|
StartedAt = day2.PostDate.AddHours(1),
|
|
CompletedAt = day2.PostDate.AddHours(2),
|
|
CreatedAt = now,
|
|
CreatedById = creatorId
|
|
};
|
|
db.Games.Add(game2a);
|
|
db.Games.Add(game2b);
|
|
await db.SaveChangesAsync();
|
|
|
|
foreach (var p in day2Participants)
|
|
{
|
|
db.Set<GamePerson>().Add(new GamePerson { GameId = game2a.Id, ClubId = clubId, PersonId = p.Id });
|
|
db.Set<GamePerson>().Add(new GamePerson { GameId = game2b.Id, ClubId = clubId, PersonId = p.Id });
|
|
}
|
|
await db.SaveChangesAsync();
|
|
|
|
// Day 3: 4 participants, 1 game (recent, still open)
|
|
var day3 = new Day
|
|
{
|
|
PostDate = now.AddDays(-1),
|
|
Status = DayStatus.Started,
|
|
ClubId = clubId,
|
|
CreatedAt = now,
|
|
CreatedById = creatorId
|
|
};
|
|
db.Days.Add(day3);
|
|
await db.SaveChangesAsync();
|
|
|
|
var day3Participants = members.Take(4).ToList();
|
|
foreach (var p in day3Participants)
|
|
{
|
|
db.Set<DayPerson>().Add(new DayPerson { DayId = day3.Id, ClubId = clubId, PersonId = p.Id });
|
|
}
|
|
|
|
var game3 = new Game
|
|
{
|
|
DayId = day3.Id,
|
|
ClubId = clubId,
|
|
GameType = "Training",
|
|
Status = GameStatus.Active,
|
|
StartedAt = day3.PostDate,
|
|
CreatedAt = now,
|
|
CreatedById = creatorId
|
|
};
|
|
db.Games.Add(game3);
|
|
await db.SaveChangesAsync();
|
|
|
|
foreach (var p in day3Participants)
|
|
{
|
|
db.Set<GamePerson>().Add(new GamePerson { GameId = game3.Id, ClubId = clubId, PersonId = p.Id });
|
|
}
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeds template GIFs for a club by copying files and creating database entries.
|
|
/// Called during demo reset and when creating new clubs.
|
|
/// </summary>
|
|
/// <param name="services">Service provider.</param>
|
|
/// <param name="clubId">Club ID to seed GIFs for.</param>
|
|
/// <param name="clubLoginName">Club's login name for folder path.</param>
|
|
public static async Task SeedTemplateGifsAsync(IServiceProvider services, Guid clubId, string clubLoginName)
|
|
{
|
|
using var scope = services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var env = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
|
|
|
|
await SeedTemplateGifsInternalAsync(db, env, clubId, clubLoginName);
|
|
}
|
|
|
|
private static async Task SeedTemplateGifsInternalAsync(
|
|
AppDbContext db,
|
|
IWebHostEnvironment env,
|
|
Guid clubId,
|
|
string clubLoginName)
|
|
{
|
|
var templatePath = Path.Combine(env.WebRootPath, "club-template", "gifs");
|
|
var templateJsonPath = Path.Combine(templatePath, "giftemplates.json");
|
|
|
|
if (!File.Exists(templateJsonPath))
|
|
{
|
|
return; // No template file, skip seeding
|
|
}
|
|
|
|
// Read template definitions
|
|
var jsonContent = await File.ReadAllTextAsync(templateJsonPath);
|
|
var templates = JsonSerializer.Deserialize<List<GifTemplateEntry>>(jsonContent, new JsonSerializerOptions
|
|
{
|
|
PropertyNameCaseInsensitive = true
|
|
});
|
|
|
|
if (templates == null || templates.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Create target directory
|
|
var targetPath = Path.Combine(env.WebRootPath, "club-media", clubLoginName, "gifs");
|
|
Directory.CreateDirectory(targetPath);
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
foreach (var template in templates)
|
|
{
|
|
var sourceFile = Path.Combine(templatePath, template.Filename);
|
|
if (!File.Exists(sourceFile))
|
|
{
|
|
continue; // Skip if source file doesn't exist
|
|
}
|
|
|
|
// Generate new filename for club
|
|
var extension = Path.GetExtension(template.Filename);
|
|
var newFileName = $"{Guid.NewGuid()}{extension}";
|
|
var targetFile = Path.Combine(targetPath, newFileName);
|
|
|
|
// Copy file
|
|
File.Copy(sourceFile, targetFile, overwrite: true);
|
|
|
|
// Parse event type
|
|
if (!Enum.TryParse<ThrowEventType>(template.ThrowEventType, out var eventType))
|
|
{
|
|
eventType = ThrowEventType.None;
|
|
}
|
|
|
|
// Get file info
|
|
var fileInfo = new FileInfo(targetFile);
|
|
|
|
// Determine content type
|
|
var contentType = extension.ToLowerInvariant() switch
|
|
{
|
|
".gif" => "image/gif",
|
|
".mp4" => "video/mp4",
|
|
".webm" => "video/webm",
|
|
_ => "application/octet-stream"
|
|
};
|
|
|
|
// Create database entry
|
|
var gif = new ClubGif
|
|
{
|
|
ClubId = clubId,
|
|
Name = template.Name,
|
|
FileName = newFileName,
|
|
OriginalFileName = template.Filename,
|
|
ContentType = contentType,
|
|
FileSizeBytes = fileInfo.Length,
|
|
AssignedEvents = eventType,
|
|
Description = template.Description,
|
|
IsEnabled = true,
|
|
IsPendingApproval = false,
|
|
RatingScore = 0,
|
|
RatingCount = 0,
|
|
CreatedAt = now
|
|
};
|
|
|
|
db.ClubGifs.Add(gif);
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes all GIF files and database entries for a club.
|
|
/// </summary>
|
|
private static async Task DeleteClubGifsAsync(AppDbContext db, IWebHostEnvironment env, Guid clubId, string clubLoginName)
|
|
{
|
|
// Delete database entries
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.ClubGifRatings WHERE ClubGifId IN (SELECT Id FROM app.ClubGifs WHERE ClubId = {clubId})");
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.ClubGifs WHERE ClubId = {clubId}");
|
|
await db.Database.ExecuteSqlAsync($"DELETE FROM app.GifSubmissionTokens WHERE ClubId = {clubId}");
|
|
|
|
// Delete files
|
|
var gifPath = Path.Combine(env.WebRootPath, "club-media", clubLoginName, "gifs");
|
|
if (Directory.Exists(gifPath))
|
|
{
|
|
Directory.Delete(gifPath, recursive: true);
|
|
}
|
|
}
|
|
}
|