Add UserActionLog and enhance registry features
Build and Push Docker Image / build-and-push (push) Successful in 1m59s
Build and Push Docker Image / build-and-push (push) Successful in 1m59s
- Introduced `UserActionLog` entity to track user actions. - Replaced `CanBeSecondHand` with `PreferSecondHand` property. - Added `ShowBankAccountName` to `RegistrySettings`. - Updated models and migrations for new properties. - Enhanced `RegistryService` with user action logging and item details. - Redesigned `Home.razor` with a grid layout and modal for registries. - Added `RegistryActionLog.razor` for admin action logs. - Improved `RegistryPublic.razor` with purchaser/contributor details. - Replaced sidebar with `TopBar.razor` for responsive navigation. - Updated CSS for new components and improved responsiveness.
This commit is contained in:
@@ -92,7 +92,15 @@ internal sealed class RegistryMetadataService(IHttpClientFactory httpClientFacto
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(image) || IsGenericAmazonImage(image))
|
||||
{
|
||||
image = ExtractAmazonImage(html) ?? image;
|
||||
var extractedImage = ExtractAmazonImage(html);
|
||||
if (!string.IsNullOrWhiteSpace(extractedImage))
|
||||
{
|
||||
image = extractedImage;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(image) && IsGenericAmazonImage(image))
|
||||
{
|
||||
image = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!price.HasValue)
|
||||
@@ -127,11 +135,15 @@ internal sealed class RegistryMetadataService(IHttpClientFactory httpClientFacto
|
||||
|
||||
private static string? ExtractAmazonImage(string html)
|
||||
{
|
||||
var patterns = new []
|
||||
var patterns = new[]
|
||||
{
|
||||
"\\\"landingImageUrl\\\"\\s*:\\s*\\\"(?<url>https?:\\\\/\\\\/[^\\\"]+)\\\"",
|
||||
"\\\"hiRes\\\"\\s*:\\s*\\\"(?<url>https?:\\\\/\\\\/[^\\\"]+)\\\"",
|
||||
"<img[^>]+id=['\"]landingImage['\"][^>]+src=['\"](?<url>[^'\"]+)['\"]"
|
||||
"data-old-hires=['\"](?<url>https?://[^'\"]+)['\"]",
|
||||
"\\\"mainUrl\\\"\\s*:\\s*\\\"(?<url>https?:\\\\/\\\\/[^\\\"]+)\\\"",
|
||||
"<img[^>]+id=['\"]landingImage['\"][^>]+src=['\"](?<url>[^'\"]+)['\"]",
|
||||
"<img[^>]+id=['\"]imgBlkFront['\"][^>]+src=['\"](?<url>[^'\"]+)['\"]",
|
||||
"\\\"image\\\"\\s*:\\s*\\\"(?<url>https?:\\\\/\\\\/[^\\\"]+)\\\""
|
||||
};
|
||||
|
||||
foreach (var pattern in patterns)
|
||||
@@ -149,7 +161,7 @@ internal sealed class RegistryMetadataService(IHttpClientFactory httpClientFacto
|
||||
}
|
||||
|
||||
value = value.Replace("\\/", "/").Replace("\\u0026", "&");
|
||||
if (Uri.TryCreate(value, UriKind.Absolute, out _))
|
||||
if (Uri.TryCreate(value, UriKind.Absolute, out _) && !IsGenericAmazonImage(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -21,8 +21,18 @@ public sealed class RegistryItemEditModel
|
||||
public int DesiredQuantity { get; set; } = 1;
|
||||
public bool ParticipationAllowed { get; set; }
|
||||
public decimal? ParticipationTargetAmount { get; set; }
|
||||
public bool CanBeSecondHand { get; set; }
|
||||
public bool? PreferSecondHand { get; set; }
|
||||
public bool IsGiven { get; set; }
|
||||
public IReadOnlyList<ItemContributorViewModel> Purchasers { get; init; } = [];
|
||||
public IReadOnlyList<ItemContributorViewModel> Contributors { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class ItemContributorViewModel
|
||||
{
|
||||
public string UserId { get; init; } = string.Empty;
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
public decimal Amount { get; init; }
|
||||
public int Quantity { get; init; }
|
||||
}
|
||||
|
||||
public sealed class RegistrySettingsEditModel
|
||||
@@ -36,6 +46,7 @@ public sealed class RegistrySettingsEditModel
|
||||
public string? BankAccountIban { get; set; }
|
||||
public string? BankAccountBic { get; set; }
|
||||
public string? BankAccountDisplayName { get; set; }
|
||||
public bool ShowBankAccountName { get; set; }
|
||||
}
|
||||
|
||||
public sealed class RegistrySummaryViewModel
|
||||
@@ -61,6 +72,9 @@ public sealed class RegistryPublicViewModel
|
||||
public string? BankAccountIban { get; init; }
|
||||
public string? BankAccountBic { get; init; }
|
||||
public string? BankAccountDisplayName { get; init; }
|
||||
public bool ShowBankAccountName { get; init; }
|
||||
public string? CurrentUserId { get; init; }
|
||||
public bool IsAdmin { get; init; }
|
||||
public IReadOnlyList<RegistryPublicItemViewModel> Items { get; init; } = [];
|
||||
}
|
||||
|
||||
@@ -78,8 +92,12 @@ public sealed class RegistryPublicItemViewModel
|
||||
public bool ParticipationAllowed { get; init; }
|
||||
public decimal? ParticipationTargetAmount { get; init; }
|
||||
public decimal MoneyFulfilledAmount { get; init; }
|
||||
public bool CanBeSecondHand { get; init; }
|
||||
public bool? PreferSecondHand { get; init; }
|
||||
public bool IsGiven { get; init; }
|
||||
public IReadOnlyList<ItemContributorViewModel> Purchasers { get; init; } = [];
|
||||
public IReadOnlyList<ItemContributorViewModel> Contributors { get; init; } = [];
|
||||
public int CurrentUserPurchasedQuantity { get; init; }
|
||||
public bool CanViewPurchasers { get; init; }
|
||||
}
|
||||
|
||||
public sealed class RegistryAdminDisplayModel
|
||||
@@ -97,3 +115,15 @@ public sealed class UrlMetadataResult
|
||||
public decimal? PriceAmount { get; init; }
|
||||
public string? CurrencyCode { get; init; }
|
||||
}
|
||||
|
||||
public sealed class RegistryActionLogViewModel
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string UserDisplayName { get; init; } = string.Empty;
|
||||
public string ActionType { get; init; } = string.Empty;
|
||||
public string? ItemName { get; init; }
|
||||
public int Quantity { get; init; }
|
||||
public decimal Amount { get; init; }
|
||||
public string? Details { get; init; }
|
||||
public DateTimeOffset CreatedAtUtc { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using BirthList.Domain.Entities;
|
||||
using BirthList.Infrastructure.Persistence;
|
||||
using BirthList.Web.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace BirthList.Web.Features.Registries;
|
||||
|
||||
internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
internal sealed class RegistryService(RegistryDbContext registryDbContext, ApplicationDbContext applicationDbContext)
|
||||
{
|
||||
public async Task<Guid> CreateRegistryAsync(string userId, RegistryCreateModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -86,16 +87,28 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
|
||||
public async Task<IReadOnlyList<RegistryAdminDisplayModel>> GetRegistryAdminsAsync(Guid registryId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await registryDbContext.RegistryAdmins
|
||||
var admins = await registryDbContext.RegistryAdmins
|
||||
.Where(x => x.RegistryId == registryId)
|
||||
.OrderBy(x => x.AddedAtUtc)
|
||||
.Select(x => new RegistryAdminDisplayModel
|
||||
{
|
||||
UserId = x.UserId,
|
||||
DisplayName = x.UserId
|
||||
})
|
||||
.Select(x => x.UserId)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var usersById = await applicationDbContext.Users
|
||||
.Where(x => admins.Contains(x.Id))
|
||||
.Select(x => new { x.Id, x.Email })
|
||||
.ToDictionaryAsync(x => x.Id, x => x.Email, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return admins
|
||||
.Select(userId => new RegistryAdminDisplayModel
|
||||
{
|
||||
UserId = userId,
|
||||
DisplayName = usersById.TryGetValue(userId, out var email) && !string.IsNullOrWhiteSpace(email)
|
||||
? email
|
||||
: userId
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<RegistryPublicViewModel?> GetPublicRegistryByCodeAsync(string code, string? userId, CancellationToken cancellationToken)
|
||||
@@ -144,6 +157,44 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var itemIds = registry.Items.Select(x => x.Id).ToList();
|
||||
var userIds = new HashSet<string>();
|
||||
|
||||
var purchases = await registryDbContext.ItemPurchases
|
||||
.Where(x => itemIds.Contains(x.RegistryItemId))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var contributions = await registryDbContext.ItemContributions
|
||||
.Where(x => itemIds.Contains(x.RegistryItemId))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
foreach (var purchase in purchases)
|
||||
{
|
||||
userIds.Add(purchase.UserId);
|
||||
}
|
||||
foreach (var contribution in contributions)
|
||||
{
|
||||
userIds.Add(contribution.UserId);
|
||||
}
|
||||
|
||||
var usersById = await applicationDbContext.Users
|
||||
.Where(x => userIds.Contains(x.Id))
|
||||
.Select(x => new { x.Id, x.Email })
|
||||
.ToDictionaryAsync(x => x.Id, x => x.Email ?? x.Id, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var purchasesByItemId = purchases
|
||||
.GroupBy(x => x.RegistryItemId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
var contributionsByItemId = contributions
|
||||
.GroupBy(x => x.RegistryItemId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
var isAdmin = !string.IsNullOrWhiteSpace(userId) && registry.Admins.Any(x => x.UserId == userId);
|
||||
|
||||
return new RegistryPublicViewModel
|
||||
{
|
||||
Id = registry.Id,
|
||||
@@ -158,6 +209,9 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
BankAccountIban = settings?.BankAccountIban,
|
||||
BankAccountBic = settings?.BankAccountBic,
|
||||
BankAccountDisplayName = settings?.BankAccountDisplayName,
|
||||
ShowBankAccountName = settings?.ShowBankAccountName ?? false,
|
||||
CurrentUserId = userId,
|
||||
IsAdmin = isAdmin,
|
||||
Items = registry.Items
|
||||
.OrderBy(x => x.Name)
|
||||
.Select(x => new RegistryPublicItemViewModel
|
||||
@@ -174,8 +228,36 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
ParticipationAllowed = x.ParticipationAllowed,
|
||||
ParticipationTargetAmount = x.ParticipationTargetAmount,
|
||||
MoneyFulfilledAmount = x.MoneyFulfilledAmount,
|
||||
CanBeSecondHand = x.CanBeSecondHand,
|
||||
IsGiven = x.IsGiven
|
||||
PreferSecondHand = x.PreferSecondHand,
|
||||
IsGiven = x.IsGiven,
|
||||
CanViewPurchasers = isAdmin || (purchasesByItemId.TryGetValue(x.Id, out var itemPurchases) && itemPurchases.Any(p => p.UserId == userId)),
|
||||
Purchasers = purchasesByItemId.TryGetValue(x.Id, out var itemPurchases2)
|
||||
? itemPurchases2
|
||||
.GroupBy(p => p.UserId)
|
||||
.Select(g => new ItemContributorViewModel
|
||||
{
|
||||
UserId = g.Key,
|
||||
DisplayName = usersById.TryGetValue(g.Key, out var displayName) ? displayName : g.Key,
|
||||
Quantity = g.Sum(p => p.Quantity)
|
||||
})
|
||||
.ToList()
|
||||
: [],
|
||||
Contributors = contributionsByItemId.TryGetValue(x.Id, out var itemContributions)
|
||||
? itemContributions
|
||||
.GroupBy(c => c.UserId)
|
||||
.Select(g => new ItemContributorViewModel
|
||||
{
|
||||
UserId = g.Key,
|
||||
DisplayName = usersById.TryGetValue(g.Key, out var displayName) ? displayName : g.Key,
|
||||
Amount = g.Sum(c => c.Amount)
|
||||
})
|
||||
.ToList()
|
||||
: [],
|
||||
CurrentUserPurchasedQuantity = purchasesByItemId.TryGetValue(x.Id, out var itemPurchases3)
|
||||
? itemPurchases3
|
||||
.Where(p => p.UserId == userId)
|
||||
.Sum(p => p.Quantity)
|
||||
: 0
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
@@ -206,7 +288,8 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
ThemeKey = registry.ThemeKey,
|
||||
BankAccountIban = settings?.BankAccountIban,
|
||||
BankAccountBic = settings?.BankAccountBic,
|
||||
BankAccountDisplayName = settings?.BankAccountDisplayName
|
||||
BankAccountDisplayName = settings?.BankAccountDisplayName,
|
||||
ShowBankAccountName = settings?.ShowBankAccountName ?? false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -241,15 +324,56 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
settings.BankAccountIban = string.IsNullOrWhiteSpace(model.BankAccountIban) ? null : model.BankAccountIban.Trim();
|
||||
settings.BankAccountBic = string.IsNullOrWhiteSpace(model.BankAccountBic) ? null : model.BankAccountBic.Trim();
|
||||
settings.BankAccountDisplayName = string.IsNullOrWhiteSpace(model.BankAccountDisplayName) ? null : model.BankAccountDisplayName.Trim();
|
||||
settings.ShowBankAccountName = model.ShowBankAccountName;
|
||||
|
||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RegistryItemEditModel>> GetRegistryItemsAsync(Guid registryId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await registryDbContext.RegistryItems
|
||||
var items = await registryDbContext.RegistryItems
|
||||
.Where(x => x.RegistryId == registryId)
|
||||
.OrderBy(x => x.Name)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var itemIds = items.Select(x => x.Id).ToList();
|
||||
var userIds = new HashSet<string>();
|
||||
|
||||
var purchases = await registryDbContext.ItemPurchases
|
||||
.Where(x => itemIds.Contains(x.RegistryItemId))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var contributions = await registryDbContext.ItemContributions
|
||||
.Where(x => itemIds.Contains(x.RegistryItemId))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
foreach (var purchase in purchases)
|
||||
{
|
||||
userIds.Add(purchase.UserId);
|
||||
}
|
||||
foreach (var contribution in contributions)
|
||||
{
|
||||
userIds.Add(contribution.UserId);
|
||||
}
|
||||
|
||||
var usersById = await applicationDbContext.Users
|
||||
.Where(x => userIds.Contains(x.Id))
|
||||
.Select(x => new { x.Id, x.Email })
|
||||
.ToDictionaryAsync(x => x.Id, x => x.Email ?? x.Id, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var purchasesByItemId = purchases
|
||||
.GroupBy(x => x.RegistryItemId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
var contributionsByItemId = contributions
|
||||
.GroupBy(x => x.RegistryItemId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
return items
|
||||
.Select(x => new RegistryItemEditModel
|
||||
{
|
||||
Id = x.Id,
|
||||
@@ -262,34 +386,104 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
DesiredQuantity = x.DesiredQuantity,
|
||||
ParticipationAllowed = x.ParticipationAllowed,
|
||||
ParticipationTargetAmount = x.ParticipationTargetAmount,
|
||||
CanBeSecondHand = x.CanBeSecondHand,
|
||||
IsGiven = x.IsGiven
|
||||
PreferSecondHand = x.PreferSecondHand,
|
||||
IsGiven = x.IsGiven,
|
||||
Purchasers = purchasesByItemId.TryGetValue(x.Id, out var itemPurchases)
|
||||
? itemPurchases
|
||||
.GroupBy(p => p.UserId)
|
||||
.Select(g => new ItemContributorViewModel
|
||||
{
|
||||
UserId = g.Key,
|
||||
DisplayName = usersById.TryGetValue(g.Key, out var displayName) ? displayName : g.Key,
|
||||
Quantity = g.Sum(p => p.Quantity)
|
||||
})
|
||||
.ToList()
|
||||
: [],
|
||||
Contributors = contributionsByItemId.TryGetValue(x.Id, out var itemContributions)
|
||||
? itemContributions
|
||||
.GroupBy(c => c.UserId)
|
||||
.Select(g => new ItemContributorViewModel
|
||||
{
|
||||
UserId = g.Key,
|
||||
DisplayName = usersById.TryGetValue(g.Key, out var displayName) ? displayName : g.Key,
|
||||
Amount = g.Sum(c => c.Amount)
|
||||
})
|
||||
.ToList()
|
||||
: []
|
||||
})
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<RegistryItemEditModel?> GetRegistryItemAsync(Guid registryId, Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await registryDbContext.RegistryItems
|
||||
.Where(x => x.RegistryId == registryId && x.Id == itemId)
|
||||
.Select(x => new RegistryItemEditModel
|
||||
{
|
||||
Id = x.Id,
|
||||
Name = x.Name,
|
||||
PictureUrl = x.PictureUrl,
|
||||
ProductUrl = x.ProductUrl,
|
||||
Description = x.Description,
|
||||
PriceAmount = x.PriceAmount,
|
||||
CurrencyCode = x.CurrencyCode,
|
||||
DesiredQuantity = x.DesiredQuantity,
|
||||
ParticipationAllowed = x.ParticipationAllowed,
|
||||
ParticipationTargetAmount = x.ParticipationTargetAmount,
|
||||
CanBeSecondHand = x.CanBeSecondHand,
|
||||
IsGiven = x.IsGiven
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
var item = await registryDbContext.RegistryItems
|
||||
.FirstOrDefaultAsync(x => x.RegistryId == registryId && x.Id == itemId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var purchases = await registryDbContext.ItemPurchases
|
||||
.Where(x => x.RegistryItemId == itemId)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var contributions = await registryDbContext.ItemContributions
|
||||
.Where(x => x.RegistryItemId == itemId)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var userIds = new HashSet<string>();
|
||||
foreach (var purchase in purchases)
|
||||
{
|
||||
userIds.Add(purchase.UserId);
|
||||
}
|
||||
foreach (var contribution in contributions)
|
||||
{
|
||||
userIds.Add(contribution.UserId);
|
||||
}
|
||||
|
||||
var usersById = await applicationDbContext.Users
|
||||
.Where(x => userIds.Contains(x.Id))
|
||||
.Select(x => new { x.Id, x.Email })
|
||||
.ToDictionaryAsync(x => x.Id, x => x.Email ?? x.Id, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new RegistryItemEditModel
|
||||
{
|
||||
Id = item.Id,
|
||||
Name = item.Name,
|
||||
PictureUrl = item.PictureUrl,
|
||||
ProductUrl = item.ProductUrl,
|
||||
Description = item.Description,
|
||||
PriceAmount = item.PriceAmount,
|
||||
CurrencyCode = item.CurrencyCode,
|
||||
DesiredQuantity = item.DesiredQuantity,
|
||||
ParticipationAllowed = item.ParticipationAllowed,
|
||||
ParticipationTargetAmount = item.ParticipationTargetAmount,
|
||||
PreferSecondHand = item.PreferSecondHand,
|
||||
IsGiven = item.IsGiven,
|
||||
Purchasers = purchases
|
||||
.GroupBy(p => p.UserId)
|
||||
.Select(g => new ItemContributorViewModel
|
||||
{
|
||||
UserId = g.Key,
|
||||
DisplayName = usersById.TryGetValue(g.Key, out var displayName) ? displayName : g.Key,
|
||||
Quantity = g.Sum(p => p.Quantity)
|
||||
})
|
||||
.ToList(),
|
||||
Contributors = contributions
|
||||
.GroupBy(c => c.UserId)
|
||||
.Select(g => new ItemContributorViewModel
|
||||
{
|
||||
UserId = g.Key,
|
||||
DisplayName = usersById.TryGetValue(g.Key, out var displayName) ? displayName : g.Key,
|
||||
Amount = g.Sum(c => c.Amount)
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task UpsertRegistryItemAsync(Guid registryId, RegistryItemEditModel model, CancellationToken cancellationToken)
|
||||
@@ -327,7 +521,7 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
entity.DesiredQuantity = model.DesiredQuantity < 1 ? 1 : model.DesiredQuantity;
|
||||
entity.ParticipationAllowed = model.ParticipationAllowed;
|
||||
entity.ParticipationTargetAmount = model.ParticipationAllowed ? model.ParticipationTargetAmount : null;
|
||||
entity.CanBeSecondHand = model.CanBeSecondHand;
|
||||
entity.PreferSecondHand = model.PreferSecondHand;
|
||||
entity.IsGiven = model.IsGiven;
|
||||
|
||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -370,6 +564,17 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
item.PurchasedQuantity += normalizedQuantity;
|
||||
item.IsGiven = item.PurchasedQuantity >= item.DesiredQuantity;
|
||||
|
||||
registryDbContext.UserActionLogs.Add(new UserActionLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RegistryId = item.RegistryId,
|
||||
UserId = userId,
|
||||
RegistryItemId = itemId,
|
||||
ActionType = UserActionType.MarkPurchased,
|
||||
Quantity = normalizedQuantity,
|
||||
CreatedAtUtc = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -397,6 +602,153 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
});
|
||||
|
||||
item.MoneyFulfilledAmount += amount;
|
||||
|
||||
registryDbContext.UserActionLogs.Add(new UserActionLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RegistryId = item.RegistryId,
|
||||
UserId = userId,
|
||||
RegistryItemId = itemId,
|
||||
ActionType = UserActionType.LogContribution,
|
||||
Amount = amount,
|
||||
Details = transferMessage,
|
||||
CreatedAtUtc = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UnmarkPurchaseAsync(Guid itemId, string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(userId);
|
||||
|
||||
var item = await registryDbContext.RegistryItems
|
||||
.FirstOrDefaultAsync(x => x.Id == itemId, cancellationToken)
|
||||
.ConfigureAwait(false) ?? throw new InvalidOperationException("Item not found.");
|
||||
|
||||
var purchase = await registryDbContext.ItemPurchases
|
||||
.FirstOrDefaultAsync(x => x.RegistryItemId == itemId && x.UserId == userId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (purchase is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
item.PurchasedQuantity -= purchase.Quantity;
|
||||
item.IsGiven = item.PurchasedQuantity >= item.DesiredQuantity;
|
||||
|
||||
registryDbContext.ItemPurchases.Remove(purchase);
|
||||
|
||||
registryDbContext.UserActionLogs.Add(new UserActionLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RegistryId = item.RegistryId,
|
||||
UserId = userId,
|
||||
RegistryItemId = itemId,
|
||||
ActionType = UserActionType.UnmarkPurchased,
|
||||
Quantity = purchase.Quantity,
|
||||
CreatedAtUtc = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UnmarkPurchaseByAdminAsync(Guid itemId, string purchaserUserId, string adminUserId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(purchaserUserId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(adminUserId);
|
||||
|
||||
var item = await registryDbContext.RegistryItems
|
||||
.FirstOrDefaultAsync(x => x.Id == itemId, cancellationToken)
|
||||
.ConfigureAwait(false) ?? throw new InvalidOperationException("Item not found.");
|
||||
|
||||
var purchase = await registryDbContext.ItemPurchases
|
||||
.FirstOrDefaultAsync(x => x.RegistryItemId == itemId && x.UserId == purchaserUserId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (purchase is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
item.PurchasedQuantity -= purchase.Quantity;
|
||||
item.IsGiven = item.PurchasedQuantity >= item.DesiredQuantity;
|
||||
|
||||
registryDbContext.ItemPurchases.Remove(purchase);
|
||||
|
||||
registryDbContext.UserActionLogs.Add(new UserActionLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RegistryId = item.RegistryId,
|
||||
UserId = adminUserId,
|
||||
RegistryItemId = itemId,
|
||||
ActionType = UserActionType.UnmarkPurchased,
|
||||
Quantity = purchase.Quantity,
|
||||
Details = $"Unmarked purchase by {purchaserUserId}",
|
||||
CreatedAtUtc = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RegistryActionLogViewModel>> GetRegistryActionLogsAsync(Guid registryId, CancellationToken cancellationToken)
|
||||
{
|
||||
var actionLogs = await registryDbContext.UserActionLogs
|
||||
.Where(x => x.RegistryId == registryId)
|
||||
.OrderByDescending(x => x.CreatedAtUtc)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var userIds = actionLogs.Select(x => x.UserId).Distinct().ToList();
|
||||
var usersById = await applicationDbContext.Users
|
||||
.Where(x => userIds.Contains(x.Id))
|
||||
.Select(x => new { x.Id, x.Email })
|
||||
.ToDictionaryAsync(x => x.Id, x => x.Email ?? x.Id, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var itemIds = actionLogs
|
||||
.Where(x => x.RegistryItemId.HasValue)
|
||||
.Select(x => x.RegistryItemId!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var itemsById = await registryDbContext.RegistryItems
|
||||
.Where(x => itemIds.Contains(x.Id))
|
||||
.Select(x => new { x.Id, x.Name })
|
||||
.ToDictionaryAsync(x => x.Id, x => x.Name, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return actionLogs
|
||||
.Select(log => new RegistryActionLogViewModel
|
||||
{
|
||||
Id = log.Id,
|
||||
UserDisplayName = usersById.TryGetValue(log.UserId, out var displayName) ? displayName : log.UserId,
|
||||
ActionType = log.ActionType.ToString(),
|
||||
ItemName = log.RegistryItemId.HasValue && itemsById.TryGetValue(log.RegistryItemId.Value, out var itemName) ? itemName : null,
|
||||
Quantity = log.Quantity,
|
||||
Amount = log.Amount,
|
||||
Details = log.Details,
|
||||
CreatedAtUtc = log.CreatedAtUtc
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task LogUserActionAsync(Guid registryId, string userId, UserActionType actionType, Guid? itemId, string? details, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(userId);
|
||||
|
||||
registryDbContext.UserActionLogs.Add(new UserActionLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RegistryId = registryId,
|
||||
UserId = userId,
|
||||
RegistryItemId = itemId,
|
||||
ActionType = actionType,
|
||||
Details = details,
|
||||
CreatedAtUtc = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -458,6 +810,21 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task RemoveRegistryAdminAsync(Guid registryId, string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(userId);
|
||||
|
||||
var admin = await registryDbContext.RegistryAdmins
|
||||
.FirstOrDefaultAsync(x => x.RegistryId == registryId && x.UserId == userId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (admin is not null)
|
||||
{
|
||||
registryDbContext.RegistryAdmins.Remove(admin);
|
||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> CreateUniquePublicCodeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
for (var attempt = 0; attempt < 10; attempt++)
|
||||
|
||||
Reference in New Issue
Block a user