Add UserActionLog and enhance registry features
Build and Push Docker Image / build-and-push (push) Failing after 12m11s
Build and Push Docker Image / build-and-push (push) Failing after 12m11s
- 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:
@@ -18,4 +18,5 @@ public class Registry
|
|||||||
public ICollection<RegistryItem> Items { get; set; } = [];
|
public ICollection<RegistryItem> Items { get; set; } = [];
|
||||||
public ICollection<RegistryVisit> Visits { get; set; } = [];
|
public ICollection<RegistryVisit> Visits { get; set; } = [];
|
||||||
public ICollection<RegistryAdminInvite> AdminInvites { get; set; } = [];
|
public ICollection<RegistryAdminInvite> AdminInvites { get; set; } = [];
|
||||||
|
public ICollection<UserActionLog> ActionLogs { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class RegistryItem
|
|||||||
public int DesiredQuantity { get; set; } = 1;
|
public int DesiredQuantity { get; set; } = 1;
|
||||||
public bool ParticipationAllowed { get; set; }
|
public bool ParticipationAllowed { get; set; }
|
||||||
public decimal? ParticipationTargetAmount { get; set; }
|
public decimal? ParticipationTargetAmount { get; set; }
|
||||||
public bool CanBeSecondHand { get; set; }
|
public bool? PreferSecondHand { get; set; }
|
||||||
public bool IsGiven { get; set; }
|
public bool IsGiven { get; set; }
|
||||||
public int PurchasedQuantity { get; set; }
|
public int PurchasedQuantity { get; set; }
|
||||||
public decimal MoneyFulfilledAmount { get; set; }
|
public decimal MoneyFulfilledAmount { get; set; }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ public class RegistrySettings
|
|||||||
public string? BankAccountIban { get; set; }
|
public string? BankAccountIban { get; set; }
|
||||||
public string? BankAccountBic { get; set; }
|
public string? BankAccountBic { get; set; }
|
||||||
public string? BankAccountDisplayName { get; set; }
|
public string? BankAccountDisplayName { get; set; }
|
||||||
|
public bool ShowBankAccountName { get; set; }
|
||||||
|
|
||||||
public Registry Registry { get; set; } = null!;
|
public Registry Registry { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace BirthList.Domain.Entities;
|
||||||
|
|
||||||
|
public class UserActionLog
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid RegistryId { get; set; }
|
||||||
|
public string UserId { get; set; } = string.Empty;
|
||||||
|
public Guid? RegistryItemId { get; set; }
|
||||||
|
public UserActionType ActionType { get; set; }
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public string? Details { get; set; }
|
||||||
|
public DateTimeOffset CreatedAtUtc { get; set; }
|
||||||
|
|
||||||
|
public Registry Registry { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum UserActionType
|
||||||
|
{
|
||||||
|
RegistryLinkOpened = 1,
|
||||||
|
ItemLinkOpened = 2,
|
||||||
|
MarkPurchased = 3,
|
||||||
|
UnmarkPurchased = 4,
|
||||||
|
MarkPartialPurchase = 5,
|
||||||
|
LogContribution = 6
|
||||||
|
}
|
||||||
@@ -6,6 +6,17 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.26" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.26" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.26" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.26">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.26" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.26" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Migrations\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
+427
@@ -0,0 +1,427 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using BirthList.Infrastructure.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthList.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(RegistryDbContext))]
|
||||||
|
[Migration("20260517141758_AddShowBankAccountNameToRegistrySettings")]
|
||||||
|
partial class AddShowBankAccountNameToRegistrySettings
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.26")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemContribution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ContributedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryItemId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TransferMessage")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryItemId");
|
||||||
|
|
||||||
|
b.ToTable("ItemContributions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemPurchase", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("PurchasedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryItemId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryItemId");
|
||||||
|
|
||||||
|
b.ToTable("ItemPurchases");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.PlatformOwner", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AssignedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("PlatformOwners");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.Registry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("BabyName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("BirthDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<string>("HeaderContentHtml")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PublicLinkCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("RegistryType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ShippingAddress")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ThemeKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(250)
|
||||||
|
.HasColumnType("nvarchar(250)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PublicLinkCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Registries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdmin", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId", "UserId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryAdmins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("RedeemedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SentToEmail")
|
||||||
|
.HasMaxLength(320)
|
||||||
|
.HasColumnType("nvarchar(320)");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("nvarchar(120)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryId");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("RegistryAdminInvites");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<bool>("CanBeSecondHand")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("DesiredQuantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsGiven")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("MoneyFulfilledAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("nvarchar(300)");
|
||||||
|
|
||||||
|
b.Property<bool>("ParticipationAllowed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ParticipationTargetAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("PictureUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("nvarchar(2048)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("ProductUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("nvarchar(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("PurchasedQuantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistrySettings", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountBic")
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountDisplayName")
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("nvarchar(120)");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountIban")
|
||||||
|
.HasMaxLength(34)
|
||||||
|
.HasColumnType("nvarchar(34)");
|
||||||
|
|
||||||
|
b.Property<bool>("ShowBankAccountName")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId");
|
||||||
|
|
||||||
|
b.ToTable("RegistrySettings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastVisitedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId", "UserId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryVisits");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemContribution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.RegistryItem", "RegistryItem")
|
||||||
|
.WithMany("Contributions")
|
||||||
|
.HasForeignKey("RegistryItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RegistryItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemPurchase", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.RegistryItem", "RegistryItem")
|
||||||
|
.WithMany("Purchases")
|
||||||
|
.HasForeignKey("RegistryItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RegistryItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdmin", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Admins")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("AdminInvites")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Items")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistrySettings", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey("BirthList.Domain.Entities.RegistrySettings", "RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Visits")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.Registry", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AdminInvites");
|
||||||
|
|
||||||
|
b.Navigation("Admins");
|
||||||
|
|
||||||
|
b.Navigation("Items");
|
||||||
|
|
||||||
|
b.Navigation("Visits");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Contributions");
|
||||||
|
|
||||||
|
b.Navigation("Purchases");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthList.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddShowBankAccountNameToRegistrySettings : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "ShowBankAccountName",
|
||||||
|
table: "RegistrySettings",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ShowBankAccountName",
|
||||||
|
table: "RegistrySettings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+427
@@ -0,0 +1,427 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using BirthList.Infrastructure.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthList.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(RegistryDbContext))]
|
||||||
|
[Migration("20260517154344_RenameCanBeSecondHandToPreferSecondHand")]
|
||||||
|
partial class RenameCanBeSecondHandToPreferSecondHand
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.26")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemContribution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ContributedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryItemId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TransferMessage")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryItemId");
|
||||||
|
|
||||||
|
b.ToTable("ItemContributions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemPurchase", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("PurchasedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryItemId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryItemId");
|
||||||
|
|
||||||
|
b.ToTable("ItemPurchases");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.PlatformOwner", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AssignedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("PlatformOwners");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.Registry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("BabyName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("BirthDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<string>("HeaderContentHtml")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PublicLinkCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("RegistryType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ShippingAddress")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ThemeKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(250)
|
||||||
|
.HasColumnType("nvarchar(250)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PublicLinkCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Registries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdmin", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId", "UserId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryAdmins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("RedeemedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SentToEmail")
|
||||||
|
.HasMaxLength(320)
|
||||||
|
.HasColumnType("nvarchar(320)");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("nvarchar(120)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryId");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("RegistryAdminInvites");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("DesiredQuantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsGiven")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("MoneyFulfilledAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("nvarchar(300)");
|
||||||
|
|
||||||
|
b.Property<bool>("ParticipationAllowed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ParticipationTargetAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("PictureUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("nvarchar(2048)");
|
||||||
|
|
||||||
|
b.Property<bool?>("PreferSecondHand")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("ProductUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("nvarchar(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("PurchasedQuantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistrySettings", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountBic")
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountDisplayName")
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("nvarchar(120)");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountIban")
|
||||||
|
.HasMaxLength(34)
|
||||||
|
.HasColumnType("nvarchar(34)");
|
||||||
|
|
||||||
|
b.Property<bool>("ShowBankAccountName")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId");
|
||||||
|
|
||||||
|
b.ToTable("RegistrySettings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastVisitedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId", "UserId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryVisits");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemContribution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.RegistryItem", "RegistryItem")
|
||||||
|
.WithMany("Contributions")
|
||||||
|
.HasForeignKey("RegistryItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RegistryItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemPurchase", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.RegistryItem", "RegistryItem")
|
||||||
|
.WithMany("Purchases")
|
||||||
|
.HasForeignKey("RegistryItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RegistryItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdmin", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Admins")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("AdminInvites")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Items")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistrySettings", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey("BirthList.Domain.Entities.RegistrySettings", "RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Visits")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.Registry", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AdminInvites");
|
||||||
|
|
||||||
|
b.Navigation("Admins");
|
||||||
|
|
||||||
|
b.Navigation("Items");
|
||||||
|
|
||||||
|
b.Navigation("Visits");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Contributions");
|
||||||
|
|
||||||
|
b.Navigation("Purchases");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthList.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RenameCanBeSecondHandToPreferSecondHand : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "CanBeSecondHand",
|
||||||
|
table: "RegistryItems",
|
||||||
|
newName: "PreferSecondHand");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<bool>(
|
||||||
|
name: "PreferSecondHand",
|
||||||
|
table: "RegistryItems",
|
||||||
|
type: "bit",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(bool),
|
||||||
|
oldType: "bit");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<bool>(
|
||||||
|
name: "PreferSecondHand",
|
||||||
|
table: "RegistryItems",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false,
|
||||||
|
oldClrType: typeof(bool),
|
||||||
|
oldType: "bit",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "PreferSecondHand",
|
||||||
|
table: "RegistryItems",
|
||||||
|
newName: "CanBeSecondHand");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+481
@@ -0,0 +1,481 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using BirthList.Infrastructure.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthList.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(RegistryDbContext))]
|
||||||
|
[Migration("20260517162502_AddUserActionLog")]
|
||||||
|
partial class AddUserActionLog
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.26")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemContribution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ContributedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryItemId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TransferMessage")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryItemId");
|
||||||
|
|
||||||
|
b.ToTable("ItemContributions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemPurchase", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("PurchasedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryItemId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryItemId");
|
||||||
|
|
||||||
|
b.ToTable("ItemPurchases");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.PlatformOwner", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AssignedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("PlatformOwners");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.Registry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("BabyName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("BirthDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<string>("HeaderContentHtml")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PublicLinkCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("RegistryType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ShippingAddress")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ThemeKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(250)
|
||||||
|
.HasColumnType("nvarchar(250)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PublicLinkCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Registries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdmin", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId", "UserId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryAdmins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("RedeemedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SentToEmail")
|
||||||
|
.HasMaxLength(320)
|
||||||
|
.HasColumnType("nvarchar(320)");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("nvarchar(120)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryId");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("RegistryAdminInvites");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("DesiredQuantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsGiven")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("MoneyFulfilledAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("nvarchar(300)");
|
||||||
|
|
||||||
|
b.Property<bool>("ParticipationAllowed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ParticipationTargetAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("PictureUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("nvarchar(2048)");
|
||||||
|
|
||||||
|
b.Property<bool?>("PreferSecondHand")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("ProductUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("nvarchar(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("PurchasedQuantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistrySettings", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountBic")
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountDisplayName")
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("nvarchar(120)");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountIban")
|
||||||
|
.HasMaxLength(34)
|
||||||
|
.HasColumnType("nvarchar(34)");
|
||||||
|
|
||||||
|
b.Property<bool>("ShowBankAccountName")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId");
|
||||||
|
|
||||||
|
b.ToTable("RegistrySettings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastVisitedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId", "UserId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryVisits");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.UserActionLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("ActionType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("Details")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RegistryItemId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryId");
|
||||||
|
|
||||||
|
b.ToTable("UserActionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemContribution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.RegistryItem", "RegistryItem")
|
||||||
|
.WithMany("Contributions")
|
||||||
|
.HasForeignKey("RegistryItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RegistryItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemPurchase", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.RegistryItem", "RegistryItem")
|
||||||
|
.WithMany("Purchases")
|
||||||
|
.HasForeignKey("RegistryItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RegistryItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdmin", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Admins")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("AdminInvites")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Items")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistrySettings", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey("BirthList.Domain.Entities.RegistrySettings", "RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Visits")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.UserActionLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("ActionLogs")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.Registry", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ActionLogs");
|
||||||
|
|
||||||
|
b.Navigation("AdminInvites");
|
||||||
|
|
||||||
|
b.Navigation("Admins");
|
||||||
|
|
||||||
|
b.Navigation("Items");
|
||||||
|
|
||||||
|
b.Navigation("Visits");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Contributions");
|
||||||
|
|
||||||
|
b.Navigation("Purchases");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthList.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserActionLog : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserActionLogs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
RegistryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
|
||||||
|
RegistryItemId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
|
ActionType = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Quantity = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Amount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
|
||||||
|
Details = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
|
CreatedAtUtc = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserActionLogs", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserActionLogs_Registries_RegistryId",
|
||||||
|
column: x => x.RegistryId,
|
||||||
|
principalTable: "Registries",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserActionLogs_RegistryId",
|
||||||
|
table: "UserActionLogs",
|
||||||
|
column: "RegistryId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserActionLogs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using BirthList.Infrastructure.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BirthList.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(RegistryDbContext))]
|
||||||
|
partial class RegistryDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.26")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemContribution", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ContributedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryItemId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("TransferMessage")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryItemId");
|
||||||
|
|
||||||
|
b.ToTable("ItemContributions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemPurchase", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("PurchasedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryItemId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryItemId");
|
||||||
|
|
||||||
|
b.ToTable("ItemPurchases");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.PlatformOwner", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AssignedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("PlatformOwners");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.Registry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("BabyName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("BirthDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<string>("HeaderContentHtml")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PublicLinkCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("RegistryType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ShippingAddress")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ThemeKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(250)
|
||||||
|
.HasColumnType("nvarchar(250)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PublicLinkCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Registries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdmin", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId", "UserId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryAdmins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("RedeemedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("SentToEmail")
|
||||||
|
.HasMaxLength(320)
|
||||||
|
.HasColumnType("nvarchar(320)");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("nvarchar(120)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryId");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("RegistryAdminInvites");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(3)
|
||||||
|
.HasColumnType("nvarchar(3)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("DesiredQuantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsGiven")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("MoneyFulfilledAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("nvarchar(300)");
|
||||||
|
|
||||||
|
b.Property<bool>("ParticipationAllowed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ParticipationTargetAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("PictureUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("nvarchar(2048)");
|
||||||
|
|
||||||
|
b.Property<bool?>("PreferSecondHand")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("ProductUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("nvarchar(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("PurchasedQuantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistrySettings", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountBic")
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountDisplayName")
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("nvarchar(120)");
|
||||||
|
|
||||||
|
b.Property<string>("BankAccountIban")
|
||||||
|
.HasMaxLength(34)
|
||||||
|
.HasColumnType("nvarchar(34)");
|
||||||
|
|
||||||
|
b.Property<bool>("ShowBankAccountName")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId");
|
||||||
|
|
||||||
|
b.ToTable("RegistrySettings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastVisitedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.HasKey("RegistryId", "UserId");
|
||||||
|
|
||||||
|
b.ToTable("RegistryVisits");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.UserActionLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("ActionType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("Details")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("RegistryId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RegistryItemId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RegistryId");
|
||||||
|
|
||||||
|
b.ToTable("UserActionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemContribution", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.RegistryItem", "RegistryItem")
|
||||||
|
.WithMany("Contributions")
|
||||||
|
.HasForeignKey("RegistryItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RegistryItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.ItemPurchase", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.RegistryItem", "RegistryItem")
|
||||||
|
.WithMany("Purchases")
|
||||||
|
.HasForeignKey("RegistryItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("RegistryItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdmin", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Admins")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("AdminInvites")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Items")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistrySettings", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey("BirthList.Domain.Entities.RegistrySettings", "RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("Visits")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.UserActionLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
|
||||||
|
.WithMany("ActionLogs")
|
||||||
|
.HasForeignKey("RegistryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Registry");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.Registry", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ActionLogs");
|
||||||
|
|
||||||
|
b.Navigation("AdminInvites");
|
||||||
|
|
||||||
|
b.Navigation("Admins");
|
||||||
|
|
||||||
|
b.Navigation("Items");
|
||||||
|
|
||||||
|
b.Navigation("Visits");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Contributions");
|
||||||
|
|
||||||
|
b.Navigation("Purchases");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ public class RegistryDbContext(DbContextOptions<RegistryDbContext> options) : Db
|
|||||||
public DbSet<ItemPurchase> ItemPurchases => Set<ItemPurchase>();
|
public DbSet<ItemPurchase> ItemPurchases => Set<ItemPurchase>();
|
||||||
public DbSet<ItemContribution> ItemContributions => Set<ItemContribution>();
|
public DbSet<ItemContribution> ItemContributions => Set<ItemContribution>();
|
||||||
public DbSet<RegistryVisit> RegistryVisits => Set<RegistryVisit>();
|
public DbSet<RegistryVisit> RegistryVisits => Set<RegistryVisit>();
|
||||||
|
public DbSet<UserActionLog> UserActionLogs => Set<UserActionLog>();
|
||||||
public DbSet<PlatformOwner> PlatformOwners => Set<PlatformOwner>();
|
public DbSet<PlatformOwner> PlatformOwners => Set<PlatformOwner>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
@@ -111,6 +112,18 @@ public class RegistryDbContext(DbContextOptions<RegistryDbContext> options) : Db
|
|||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<UserActionLog>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(x => x.Id);
|
||||||
|
entity.Property(x => x.UserId).HasMaxLength(450);
|
||||||
|
entity.Property(x => x.Amount).HasPrecision(18, 2);
|
||||||
|
entity.Property(x => x.Details).HasMaxLength(500);
|
||||||
|
entity.HasOne(x => x.Registry)
|
||||||
|
.WithMany(x => x.ActionLogs)
|
||||||
|
.HasForeignKey(x => x.RegistryId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<PlatformOwner>(entity =>
|
modelBuilder.Entity<PlatformOwner>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(x => x.Id);
|
entity.HasKey(x => x.Id);
|
||||||
|
|||||||
@@ -6,11 +6,12 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
|
||||||
<link rel="stylesheet" href="app.css" />
|
<link rel="stylesheet" href="app.css" />
|
||||||
<link rel="stylesheet" href="BirthList.Web.styles.css" />
|
<link rel="stylesheet" href="BirthList.Web.styles.css" />
|
||||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet" />
|
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet" />
|
||||||
<link href="https://cdn.quilljs.com/1.3.6/quill.bubble.css" rel="stylesheet" />
|
<link href="https://cdn.quilljs.com/1.3.6/quill.bubble.css" rel="stylesheet" />
|
||||||
<link rel="icon" type="image/png" href="favicon.png" />
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%230d6efd' d='M3 2.5a2.5 2.5 0 0 1 5 0 2.5 2.5 0 0 1 5 0v.006c0 .07 0 .27-.038.494H15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 14.5V7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2.038A.968.968 0 0 1 3 2.506V2.5zM4 3h3V2.5a1.5 1.5 0 1 0-3 0V3zm5 0h3V2.5a1.5 1.5 0 0 0-3 0V3zM1 6h14V4H1v2zm1 1v7.5a.5.5 0 0 0 .5.5H7V7H2zm6 8h5.5a.5.5 0 0 0 .5-.5V7H8v8z'/%3E%3C/svg%3E" />
|
||||||
<HeadOutlet />
|
<HeadOutlet />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
<div class="page">
|
<TopBar />
|
||||||
<div class="sidebar">
|
|
||||||
<NavMenu />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main>
|
<main class="main-content">
|
||||||
<div class="top-row px-4">
|
<article class="content px-4">
|
||||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
@Body
|
||||||
</div>
|
</article>
|
||||||
|
</main>
|
||||||
<article class="content px-4">
|
|
||||||
@Body
|
|
||||||
</article>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="blazor-error-ui">
|
<div id="blazor-error-ui">
|
||||||
An unhandled error has occurred.
|
An unhandled error has occurred.
|
||||||
|
|||||||
@@ -2,78 +2,18 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row {
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
border-bottom: 1px solid #d6d5d5;
|
|
||||||
justify-content: flex-end;
|
|
||||||
height: 3.5rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
.content {
|
||||||
white-space: nowrap;
|
padding-top: 2rem;
|
||||||
margin-left: 1.5rem;
|
padding-bottom: 2rem;
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a:first-child {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640.98px) {
|
|
||||||
.top-row {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
|
||||||
.page {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
height: 100vh;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row.auth ::deep a:first-child {
|
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row, article {
|
|
||||||
padding-left: 2rem !important;
|
|
||||||
padding-right: 1.5rem !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#blazor-error-ui {
|
#blazor-error-ui {
|
||||||
@@ -82,15 +22,19 @@ main {
|
|||||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
display: none;
|
display: none;
|
||||||
left: 0;
|
left: 0;
|
||||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
padding: 20px 40px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100%;
|
right: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
#blazor-error-ui .dismiss {
|
#blazor-error-ui .dismiss {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0.75rem;
|
right: 3.5rem;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||||
|
|
||||||
|
<nav class="top-bar">
|
||||||
|
<div class="top-bar-left">
|
||||||
|
<a href="/" class="top-bar-brand" title="Gift List">
|
||||||
|
<i class="bi bi-gift" aria-hidden="true"></i>
|
||||||
|
<span class="brand-text">Gift List</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="top-bar-right">
|
||||||
|
<AuthorizeView>
|
||||||
|
<Authorized>
|
||||||
|
<div class="user-info">
|
||||||
|
<i class="bi bi-person-circle" aria-hidden="true"></i>
|
||||||
|
<span class="user-email">@context.User.Identity?.Name</span>
|
||||||
|
</div>
|
||||||
|
<a href="Account/Manage" class="top-bar-link" title="Account settings">
|
||||||
|
<i class="bi bi-gear" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
<form action="Account/Logout" method="post" class="logout-form">
|
||||||
|
<AntiforgeryToken />
|
||||||
|
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
|
||||||
|
<button type="submit" class="top-bar-link logout-btn" title="Sign out">
|
||||||
|
<i class="bi bi-box-arrow-right" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<a href="Account/Login" class="top-bar-link" title="Sign in">
|
||||||
|
<i class="bi bi-box-arrow-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string currentUrl = "";
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
currentUrl = GetRelativePath(NavigationManager.Uri);
|
||||||
|
NavigationManager.LocationChanged += OnLocationChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||||
|
{
|
||||||
|
currentUrl = GetRelativePath(e.Location);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetRelativePath(string absoluteUri)
|
||||||
|
{
|
||||||
|
var relativePath = NavigationManager.ToBaseRelativePath(absoluteUri);
|
||||||
|
if (string.IsNullOrWhiteSpace(relativePath))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove leading slash if present since the logout endpoint adds ~/ prefix
|
||||||
|
if (relativePath.StartsWith('/'))
|
||||||
|
{
|
||||||
|
return relativePath.Substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDisposable.Dispose()
|
||||||
|
{
|
||||||
|
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
.top-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 3.5rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-brand:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.brand-text {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
max-width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.user-email {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-link {
|
||||||
|
color: white;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-link:hover,
|
||||||
|
.logout-btn:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-form {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
@@ -1,86 +1,172 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
|
|
||||||
|
@using BirthList.Web.Features.Registries
|
||||||
|
|
||||||
<PageTitle>Birth Registry</PageTitle>
|
<PageTitle>Birth Registry</PageTitle>
|
||||||
|
|
||||||
<h1>Birth Registry</h1>
|
<h1>Welcome to Gift List</h1>
|
||||||
|
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized Context="authState">
|
<Authorized Context="authState">
|
||||||
<div class="mb-4">
|
<div class="registry-sections">
|
||||||
<h2>Create your registry</h2>
|
<div class="mb-4">
|
||||||
<EditForm Model="Model" OnValidSubmit="CreateRegistryAsync" Context="formContext" FormName="create-registry-form">
|
<div class="section-header">
|
||||||
<DataAnnotationsValidator />
|
<h2>Registries you manage</h2>
|
||||||
<div class="row g-3">
|
<button class="btn btn-primary btn-sm" @onclick="() => ShowCreateForm = !ShowCreateForm">
|
||||||
<div class="col-md-5">
|
<span class="bi bi-plus"></span> Create new
|
||||||
<label class="form-label">Title</label>
|
</button>
|
||||||
<InputText class="form-control" @bind-Value="Model.Title" />
|
</div>
|
||||||
|
@if (MyRegistries.Count == 0)
|
||||||
|
{
|
||||||
|
<p class="text-muted">No registries yet.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="registry-grid">
|
||||||
|
@foreach (var registry in MyRegistries)
|
||||||
|
{
|
||||||
|
<div class="registry-card">
|
||||||
|
<h3>@registry.Title</h3>
|
||||||
|
<div class="registry-actions">
|
||||||
|
<a href="/registry/@registry.PublicLinkCode" class="btn btn-outline-primary btn-sm">View</a>
|
||||||
|
<a href="/registry/@registry.Id/admin" class="btn btn-outline-secondary btn-sm">Manage</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
}
|
||||||
<label class="form-label">Type</label>
|
</div>
|
||||||
<InputSelect class="form-select" @bind-Value="Model.RegistryType">
|
|
||||||
<option value="@BirthList.Domain.Entities.RegistryType.Birth">Birth</option>
|
<div class="mb-4">
|
||||||
<option value="@BirthList.Domain.Entities.RegistryType.Wedding">Wedding</option>
|
<h2>Visited registries</h2>
|
||||||
<option value="@BirthList.Domain.Entities.RegistryType.Birthday">Birthday</option>
|
@if (VisitedRegistries.Count == 0)
|
||||||
</InputSelect>
|
{
|
||||||
|
<p class="text-muted">No visited registries yet.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="registry-grid">
|
||||||
|
@foreach (var registry in VisitedRegistries)
|
||||||
|
{
|
||||||
|
<div class="registry-card">
|
||||||
|
<h3>@registry.Title</h3>
|
||||||
|
<div class="registry-actions">
|
||||||
|
<a href="/registry/@registry.PublicLinkCode" class="btn btn-outline-primary btn-sm">View</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
}
|
||||||
<label class="form-label">Theme</label>
|
</div>
|
||||||
<InputSelect class="form-select" @bind-Value="Model.ThemeKey">
|
</div>
|
||||||
<option value="default">Default</option>
|
|
||||||
<option value="soft">Soft</option>
|
@if (ShowCreateForm)
|
||||||
<option value="modern">Modern</option>
|
{
|
||||||
</InputSelect>
|
<div class="create-registry-modal-overlay" @onclick="() => ShowCreateForm = false">
|
||||||
|
<div class="create-registry-modal" @onclick:stopPropagation="true">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Create new registry</h3>
|
||||||
|
<button type="button" class="btn-close" @onclick="() => ShowCreateForm = false"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 d-flex align-items-end">
|
<div class="modal-body">
|
||||||
<button class="btn btn-primary w-100" type="submit">Create</button>
|
<EditForm Model="Model" OnValidSubmit="CreateRegistryAsync" Context="formContext" FormName="create-registry-form">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Title</label>
|
||||||
|
<InputText class="form-control" @bind-Value="Model.Title" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Type</label>
|
||||||
|
<InputSelect class="form-select" @bind-Value="Model.RegistryType">
|
||||||
|
<option value="@BirthList.Domain.Entities.RegistryType.Birth">Birth</option>
|
||||||
|
<option value="@BirthList.Domain.Entities.RegistryType.Wedding">Wedding</option>
|
||||||
|
<option value="@BirthList.Domain.Entities.RegistryType.Birthday">Birthday</option>
|
||||||
|
</InputSelect>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Theme</label>
|
||||||
|
<InputSelect class="form-select" @bind-Value="Model.ThemeKey">
|
||||||
|
<option value="default">Default</option>
|
||||||
|
<option value="soft">Soft</option>
|
||||||
|
<option value="modern">Modern</option>
|
||||||
|
</InputSelect>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger mb-3">@ErrorMessage</div>
|
||||||
|
}
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" @onclick="() => ShowCreateForm = false">Cancel</button>
|
||||||
|
<button class="btn btn-primary" type="submit">Create</button>
|
||||||
|
</div>
|
||||||
|
</EditForm>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</EditForm>
|
</div>
|
||||||
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
}
|
||||||
{
|
|
||||||
<div class="alert alert-danger mt-3">@ErrorMessage</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h2>Registries you manage</h2>
|
|
||||||
@if (MyRegistries.Count == 0)
|
|
||||||
{
|
|
||||||
<p>No registries yet.</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<ul>
|
|
||||||
@foreach (var registry in MyRegistries)
|
|
||||||
{
|
|
||||||
<li>
|
|
||||||
<a href="/registry/@registry.PublicLinkCode">@registry.Title</a>
|
|
||||||
|
|
|
||||||
<a href="/registry/@registry.Id/admin">Admin</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2>Visited registries</h2>
|
|
||||||
@if (VisitedRegistries.Count == 0)
|
|
||||||
{
|
|
||||||
<p>No visited registries yet.</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<ul>
|
|
||||||
@foreach (var registry in VisitedRegistries)
|
|
||||||
{
|
|
||||||
<li><a href="/registry/@registry.PublicLinkCode">@registry.Title</a></li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
<p>Please <a href="Account/Login">log in</a> to create and manage registries.</p>
|
<div class="alert alert-info mt-4">
|
||||||
|
<p>Please <a href="Account/Login">log in</a> to create and manage registries.</p>
|
||||||
|
</div>
|
||||||
</NotAuthorized>
|
</NotAuthorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[SupplyParameterFromForm(FormName = "create-registry-form")]
|
||||||
|
protected RegistryCreateModel Model { get; set; } = new();
|
||||||
|
|
||||||
|
protected IReadOnlyList<RegistrySummaryViewModel> MyRegistries { get; private set; } = [];
|
||||||
|
protected IReadOnlyList<RegistrySummaryViewModel> VisitedRegistries { get; private set; } = [];
|
||||||
|
protected string? ErrorMessage { get; private set; }
|
||||||
|
protected bool ShowCreateForm { get; private set; }
|
||||||
|
|
||||||
|
[Inject] private RegistryService RegistryService { get; set; } = null!;
|
||||||
|
[Inject] private RegistryUserContext RegistryUserContext { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task CreateRegistryAsync()
|
||||||
|
{
|
||||||
|
ErrorMessage = null;
|
||||||
|
|
||||||
|
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
ErrorMessage = "You must be logged in to create a registry.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Model.Title))
|
||||||
|
{
|
||||||
|
ErrorMessage = "Title is required.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await RegistryService.CreateRegistryAsync(userId, Model, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
Model = new RegistryCreateModel
|
||||||
|
{
|
||||||
|
RegistryType = Model.RegistryType,
|
||||||
|
ThemeKey = Model.ThemeKey
|
||||||
|
};
|
||||||
|
ShowCreateForm = false;
|
||||||
|
await LoadAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAsync()
|
||||||
|
{
|
||||||
|
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
MyRegistries = [];
|
||||||
|
VisitedRegistries = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MyRegistries = await RegistryService.GetAdminRegistriesAsync(userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
VisitedRegistries = await RegistryService.GetVisitedRegistriesAsync(userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
using BirthList.Web.Features.Registries;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
|
|
||||||
namespace BirthList.Web.Components.Pages;
|
|
||||||
|
|
||||||
public partial class Home : ComponentBase
|
|
||||||
{
|
|
||||||
[Inject] private RegistryService RegistryService { get; set; } = null!;
|
|
||||||
[Inject] private RegistryUserContext RegistryUserContext { get; set; } = null!;
|
|
||||||
|
|
||||||
[SupplyParameterFromForm(FormName = "create-registry-form")]
|
|
||||||
protected RegistryCreateModel Model { get; set; } = new();
|
|
||||||
|
|
||||||
protected IReadOnlyList<RegistrySummaryViewModel> MyRegistries { get; private set; } = [];
|
|
||||||
protected IReadOnlyList<RegistrySummaryViewModel> VisitedRegistries { get; private set; } = [];
|
|
||||||
protected string? ErrorMessage { get; private set; }
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
await LoadAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task CreateRegistryAsync()
|
|
||||||
{
|
|
||||||
ErrorMessage = null;
|
|
||||||
|
|
||||||
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
if (string.IsNullOrWhiteSpace(userId))
|
|
||||||
{
|
|
||||||
ErrorMessage = "You must be logged in to create a registry.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(Model.Title))
|
|
||||||
{
|
|
||||||
ErrorMessage = "Title is required.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await RegistryService.CreateRegistryAsync(userId, Model, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
Model = new RegistryCreateModel
|
|
||||||
{
|
|
||||||
RegistryType = Model.RegistryType,
|
|
||||||
ThemeKey = Model.ThemeKey
|
|
||||||
};
|
|
||||||
await LoadAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadAsync()
|
|
||||||
{
|
|
||||||
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
if (string.IsNullOrWhiteSpace(userId))
|
|
||||||
{
|
|
||||||
MyRegistries = [];
|
|
||||||
VisitedRegistries = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MyRegistries = await RegistryService.GetAdminRegistriesAsync(userId, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
VisitedRegistries = await RegistryService.GetVisitedRegistriesAsync(userId, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,142 @@
|
|||||||
h1 {
|
h1 {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-sections {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-card:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-card h3 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #212529;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-actions .btn {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal overlay */
|
||||||
|
.create-registry-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-registry-modal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.registry-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
@page "/registry/{RegistryId:guid}/admin/action-log"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
@using BirthList.Web.Features.Registries
|
||||||
|
@using BirthList.Web.Authorization
|
||||||
|
|
||||||
|
<PageTitle>Action Log - Registry Admin</PageTitle>
|
||||||
|
|
||||||
|
@if (!IsAuthorized)
|
||||||
|
{
|
||||||
|
<p>Access denied.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<h1>Registry Action Log</h1>
|
||||||
|
<p class="text-muted">This log shows all user actions on this registry: purchases, contributions, and other interactions.</p>
|
||||||
|
|
||||||
|
@if (ActionLogs is null)
|
||||||
|
{
|
||||||
|
<p>Loading...</p>
|
||||||
|
}
|
||||||
|
else if (ActionLogs.Count == 0)
|
||||||
|
{
|
||||||
|
<p class="text-muted">No actions recorded yet.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date/Time</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Details</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var log in ActionLogs)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@(log.CreatedAtUtc.LocalDateTime.ToString("d")) @(log.CreatedAtUtc.LocalDateTime.ToString("T"))</td>
|
||||||
|
<td>@log.UserDisplayName</td>
|
||||||
|
<td>
|
||||||
|
@switch (log.ActionType)
|
||||||
|
{
|
||||||
|
case "RegistryLinkOpened":
|
||||||
|
<span class="badge bg-info">Registry opened</span>
|
||||||
|
break;
|
||||||
|
case "ItemLinkOpened":
|
||||||
|
<span class="badge bg-info">Item link opened</span>
|
||||||
|
break;
|
||||||
|
case "MarkPurchased":
|
||||||
|
<span class="badge bg-success">Purchase marked</span>
|
||||||
|
break;
|
||||||
|
case "UnmarkPurchased":
|
||||||
|
<span class="badge bg-warning">Purchase unmarked</span>
|
||||||
|
break;
|
||||||
|
case "MarkPartialPurchase":
|
||||||
|
<span class="badge bg-primary">Partial purchase</span>
|
||||||
|
break;
|
||||||
|
case "LogContribution":
|
||||||
|
<span class="badge bg-secondary">Contribution logged</span>
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
<span class="badge bg-dark">@log.ActionType</span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>@(log.ItemName ?? "-")</td>
|
||||||
|
<td>@(log.Quantity > 0 ? log.Quantity.ToString() : "-")</td>
|
||||||
|
<td>@(log.Amount > 0 ? log.Amount.ToString("0.00") : "-")</td>
|
||||||
|
<td>@(string.IsNullOrWhiteSpace(log.Details) ? "-" : log.Details)</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public Guid RegistryId { get; set; }
|
||||||
|
|
||||||
|
[Inject] private RegistryService RegistryService { get; set; } = null!;
|
||||||
|
[Inject] private RegistryAuthorizationService AuthorizationService { get; set; } = null!;
|
||||||
|
[Inject] private RegistryUserContext RegistryUserContext { get; set; } = null!;
|
||||||
|
|
||||||
|
private IReadOnlyList<RegistryActionLogViewModel>? ActionLogs { get; set; }
|
||||||
|
private bool IsAuthorized { get; set; }
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
IsAuthorized = await AuthorizationService.IsRegistryAdminAsync(RegistryId, userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (IsAuthorized)
|
||||||
|
{
|
||||||
|
ActionLogs = await RegistryService.GetRegistryActionLogsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,172 +20,289 @@ else
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="mb-4">
|
<ul class="nav nav-tabs mb-4" role="tablist">
|
||||||
<h2>Current admins</h2>
|
<li class="nav-item" role="presentation">
|
||||||
@if (Admins.Count == 0)
|
<button class="nav-link @(GetTabClass("items"))"
|
||||||
{
|
id="items-tab"
|
||||||
<p>No admins assigned yet.</p>
|
@onclick='() => SetActiveTab("items")'
|
||||||
}
|
type="button"
|
||||||
else
|
role="tab"
|
||||||
{
|
aria-controls="items-content"
|
||||||
<ul>
|
aria-selected="@(ActiveTab == "items" ? "true" : "false")">
|
||||||
@foreach (var admin in Admins)
|
<span class="bi bi-box-seam"></span> Items
|
||||||
{
|
</button>
|
||||||
<li>@admin.DisplayName</li>
|
</li>
|
||||||
}
|
<li class="nav-item" role="presentation">
|
||||||
</ul>
|
<button class="nav-link @(GetTabClass("settings"))"
|
||||||
}
|
id="settings-tab"
|
||||||
</section>
|
@onclick='() => SetActiveTab("settings")'
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-controls="settings-content"
|
||||||
|
aria-selected="@(ActiveTab == "settings" ? "true" : "false")">
|
||||||
|
<span class="bi bi-gear"></span> Settings
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link @(GetTabClass("admins"))"
|
||||||
|
id="admins-tab"
|
||||||
|
@onclick='() => SetActiveTab("admins")'
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-controls="admins-content"
|
||||||
|
aria-selected="@(ActiveTab == "admins" ? "true" : "false")">
|
||||||
|
<span class="bi bi-people"></span> Administrators
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a href="/registry/@RegistryId/admin/action-log" class="nav-link">
|
||||||
|
<span class="bi bi-clock-history"></span> Action Log
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<section class="mb-4">
|
<div class="tab-content">
|
||||||
<h2>Settings</h2>
|
<!-- Items Tab -->
|
||||||
<EditForm Model="SettingsModel" OnValidSubmit="SaveSettingsAsync" FormName="registry-settings-form">
|
<div class="tab-pane fade @(GetTabPaneClass("items"))" id="items-content" role="tabpanel" aria-labelledby="items-tab">
|
||||||
<div class="row g-3">
|
<section class="mb-4">
|
||||||
<div class="col-md-4">
|
<h2>Add or edit item</h2>
|
||||||
<label class="form-label">Baby name</label>
|
<EditForm Model="ItemModel" OnValidSubmit="SaveItemAsync" FormName="registry-item-form">
|
||||||
<InputText class="form-control" @bind-Value="SettingsModel.BabyName" />
|
<div class="row g-3">
|
||||||
</div>
|
<div class="col-md-6">
|
||||||
<div class="col-md-3">
|
<label class="form-label">Name</label>
|
||||||
<label class="form-label">Birth date</label>
|
<InputText class="form-control" @bind-Value="ItemModel.Name" />
|
||||||
<InputDate class="form-control" @bind-Value="SettingsModel.BirthDate" />
|
</div>
|
||||||
</div>
|
<div class="col-md-6">
|
||||||
<div class="col-md-2">
|
<label class="form-label">Product URL</label>
|
||||||
<label class="form-label">Currency</label>
|
<div class="input-group">
|
||||||
<InputText class="form-control" @bind-Value="SettingsModel.CurrencyCode" />
|
<InputText class="form-control" @bind-Value="ItemModel.ProductUrl" />
|
||||||
</div>
|
<button type="button" class="btn btn-outline-secondary" @onclick="FetchMetadataAsync">Auto fetch</button>
|
||||||
<div class="col-md-3">
|
</div>
|
||||||
<label class="form-label">Theme</label>
|
</div>
|
||||||
<InputSelect class="form-select" @bind-Value="SettingsModel.ThemeKey">
|
<div class="col-md-6">
|
||||||
<option value="default">Default</option>
|
<label class="form-label">Picture URL</label>
|
||||||
<option value="soft">Soft</option>
|
<InputText class="form-control" @bind-Value="ItemModel.PictureUrl" />
|
||||||
<option value="modern">Modern</option>
|
</div>
|
||||||
</InputSelect>
|
<div class="col-md-6">
|
||||||
</div>
|
<label class="form-label">Description</label>
|
||||||
</div>
|
<InputTextArea class="form-control" @bind-Value="ItemModel.Description" rows="2" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Price</label>
|
||||||
|
<InputNumber class="form-control" @bind-Value="ItemModel.PriceAmount" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Currency</label>
|
||||||
|
<InputText class="form-control" @bind-Value="ItemModel.CurrencyCode" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Desired qty</label>
|
||||||
|
<InputNumber class="form-control" @bind-Value="ItemModel.DesiredQuantity" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 d-flex align-items-center gap-2">
|
||||||
|
<InputCheckbox @bind-Value="ItemModel.ParticipationAllowed" />
|
||||||
|
<label>Participation</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Participation target</label>
|
||||||
|
<InputNumber class="form-control" @bind-Value="ItemModel.ParticipationTargetAmount" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Second hand preference</label>
|
||||||
|
<InputSelect class="form-select" @bind-Value="ItemModel.PreferSecondHand">
|
||||||
|
<option value="">Second hand optional</option>
|
||||||
|
<option value="true">Prefer second hand</option>
|
||||||
|
<option value="false">New only</option>
|
||||||
|
</InputSelect>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 d-flex align-items-center gap-2">
|
||||||
|
<InputCheckbox @bind-Value="ItemModel.IsGiven" />
|
||||||
|
<label>Given</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary mt-3" type="submit">Save item</button>
|
||||||
|
</EditForm>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="mt-3">
|
<section>
|
||||||
<label class="form-label">Shipping address</label>
|
<h2>Items</h2>
|
||||||
<InputTextArea class="form-control" @bind-Value="SettingsModel.ShippingAddress" rows="3" />
|
<table class="table table-striped">
|
||||||
</div>
|
<thead>
|
||||||
|
<tr>
|
||||||
<div class="mt-3">
|
<th>Name</th>
|
||||||
<label class="form-label">Top content</label>
|
<th>Desired Qty</th>
|
||||||
<BlazoredTextEditor @ref="TextEditor" Placeholder="Welcome text" Theme="snow" />
|
<th>Participation</th>
|
||||||
</div>
|
<th>Purchased by / Contributed by</th>
|
||||||
|
<th></th>
|
||||||
<div class="row g-3 mt-2">
|
</tr>
|
||||||
<div class="col-md-4">
|
</thead>
|
||||||
<label class="form-label">Bank account name</label>
|
<tbody>
|
||||||
<InputText class="form-control" @bind-Value="SettingsModel.BankAccountDisplayName" />
|
@foreach (var item in Items)
|
||||||
</div>
|
{
|
||||||
<div class="col-md-4">
|
<tr>
|
||||||
<label class="form-label">IBAN</label>
|
<td>@item.Name</td>
|
||||||
<InputText class="form-control" @bind-Value="SettingsModel.BankAccountIban" />
|
<td>@item.DesiredQuantity</td>
|
||||||
</div>
|
<td>@(item.ParticipationAllowed ? "Yes" : "No")</td>
|
||||||
<div class="col-md-4">
|
<td>
|
||||||
<label class="form-label">BIC</label>
|
@if (item.Purchasers.Count > 0)
|
||||||
<InputText class="form-control" @bind-Value="SettingsModel.BankAccountBic" />
|
{
|
||||||
</div>
|
<div class="mb-1">
|
||||||
</div>
|
<small><strong>Purchased:</strong></small>
|
||||||
|
<div class="small">
|
||||||
<button class="btn btn-primary mt-3" type="submit">Save settings</button>
|
@foreach (var purchaser in item.Purchasers)
|
||||||
</EditForm>
|
{
|
||||||
</section>
|
<div>@purchaser.DisplayName (@purchaser.Quantity)</div>
|
||||||
|
}
|
||||||
<section class="mb-4">
|
</div>
|
||||||
<h2>Invite admin</h2>
|
</div>
|
||||||
<div class="row g-2">
|
}
|
||||||
<div class="col-md-8">
|
@if (item.Contributors.Count > 0)
|
||||||
<InputText class="form-control" @bind-Value="InviteEmail" placeholder="optional email" />
|
{
|
||||||
</div>
|
<div>
|
||||||
<div class="col-md-4">
|
<small><strong>Contributed:</strong></small>
|
||||||
<button class="btn btn-outline-primary w-100" @onclick="CreateInviteAsync">Create invite</button>
|
<div class="small">
|
||||||
</div>
|
@foreach (var contributor in item.Contributors)
|
||||||
|
{
|
||||||
|
<div>@contributor.DisplayName (@contributor.Amount.ToString("0.00"))</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary me-2" @onclick="() => EditItem(item)">Edit</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteItemAsync(item.Id)">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@if (!string.IsNullOrWhiteSpace(InviteLink))
|
|
||||||
{
|
|
||||||
<p class="mt-2">Invite link: <a href="@InviteLink">@InviteLink</a></p>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
<!-- Settings Tab -->
|
||||||
<h2>Add or edit item</h2>
|
<div class="tab-pane fade @(GetTabPaneClass("settings"))" id="settings-content" role="tabpanel" aria-labelledby="settings-tab">
|
||||||
<EditForm Model="ItemModel" OnValidSubmit="SaveItemAsync" FormName="registry-item-form">
|
<section class="mb-4">
|
||||||
<div class="row g-3">
|
<h2>Registry Settings</h2>
|
||||||
<div class="col-md-6">
|
<EditForm Model="SettingsModel" OnValidSubmit="SaveSettingsAsync" FormName="registry-settings-form">
|
||||||
<label class="form-label">Name</label>
|
<div class="row g-3">
|
||||||
<InputText class="form-control" @bind-Value="ItemModel.Name" />
|
<div class="col-md-4">
|
||||||
</div>
|
<label class="form-label">Baby name</label>
|
||||||
<div class="col-md-6">
|
<InputText class="form-control" @bind-Value="SettingsModel.BabyName" />
|
||||||
<label class="form-label">Product URL</label>
|
</div>
|
||||||
<div class="input-group">
|
<div class="col-md-3">
|
||||||
<InputText class="form-control" @bind-Value="ItemModel.ProductUrl" />
|
<label class="form-label">Birth date</label>
|
||||||
<button type="button" class="btn btn-outline-secondary" @onclick="FetchMetadataAsync">Auto fetch</button>
|
<InputDate class="form-control" @bind-Value="SettingsModel.BirthDate" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label">Currency</label>
|
||||||
|
<InputText class="form-control" @bind-Value="SettingsModel.CurrencyCode" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Theme</label>
|
||||||
|
<InputSelect class="form-select" @bind-Value="SettingsModel.ThemeKey">
|
||||||
|
<option value="default">Default</option>
|
||||||
|
<option value="soft">Soft</option>
|
||||||
|
<option value="modern">Modern</option>
|
||||||
|
</InputSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<label class="form-label">Shipping address</label>
|
||||||
|
<InputTextArea class="form-control" @bind-Value="SettingsModel.ShippingAddress" rows="4" />
|
||||||
|
<small class="form-text text-muted">Line breaks will be preserved</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<label class="form-label">Top content</label>
|
||||||
|
<div class="editor-wrapper">
|
||||||
|
<BlazoredTextEditor @ref="TextEditor" Placeholder="Welcome text" Theme="snow" ToolbarContent="@ToolbarContent" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<h3>Bank Account Settings</h3>
|
||||||
|
<div class="row g-3 mt-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Bank account name</label>
|
||||||
|
<InputText class="form-control" @bind-Value="SettingsModel.BankAccountDisplayName" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">IBAN</label>
|
||||||
|
<InputText class="form-control" @bind-Value="SettingsModel.BankAccountIban" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">BIC</label>
|
||||||
|
<InputText class="form-control" @bind-Value="SettingsModel.BankAccountBic" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="form-check">
|
||||||
|
<InputCheckbox class="form-check-input" @bind-Value="SettingsModel.ShowBankAccountName" id="showBankName" />
|
||||||
|
<label class="form-check-label" for="showBankName">
|
||||||
|
Display bank account name
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary mt-4" type="submit">Save settings</button>
|
||||||
|
</EditForm>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admins Tab -->
|
||||||
|
<div class="tab-pane fade @(GetTabPaneClass("admins"))" id="admins-content" role="tabpanel" aria-labelledby="admins-tab">
|
||||||
|
<section class="mb-4">
|
||||||
|
<h2>Current administrators</h2>
|
||||||
|
@if (Admins.Count == 0)
|
||||||
|
{
|
||||||
|
<p class="text-muted">No admins assigned yet.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Email / Name</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var admin in Admins)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@admin.DisplayName</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteAdminAsync(admin.UserId)">Remove</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Invite administrator</h2>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<InputText class="form-control" @bind-Value="InviteEmail" placeholder="optional email" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button class="btn btn-outline-primary w-100" @onclick="CreateInviteAsync">Create invite</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
@if (!string.IsNullOrWhiteSpace(InviteLink))
|
||||||
<label class="form-label">Picture URL</label>
|
|
||||||
<InputText class="form-control" @bind-Value="ItemModel.PictureUrl" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Description</label>
|
|
||||||
<InputTextArea class="form-control" @bind-Value="ItemModel.Description" rows="2" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Price</label>
|
|
||||||
<InputNumber class="form-control" @bind-Value="ItemModel.PriceAmount" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Currency</label>
|
|
||||||
<InputText class="form-control" @bind-Value="ItemModel.CurrencyCode" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Desired qty</label>
|
|
||||||
<InputNumber class="form-control" @bind-Value="ItemModel.DesiredQuantity" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 d-flex align-items-center gap-2">
|
|
||||||
<InputCheckbox @bind-Value="ItemModel.ParticipationAllowed" />
|
|
||||||
<label>Participation</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Participation target</label>
|
|
||||||
<InputNumber class="form-control" @bind-Value="ItemModel.ParticipationTargetAmount" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 d-flex align-items-center gap-2">
|
|
||||||
<InputCheckbox @bind-Value="ItemModel.CanBeSecondHand" />
|
|
||||||
<label>Second hand allowed</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 d-flex align-items-center gap-2">
|
|
||||||
<InputCheckbox @bind-Value="ItemModel.IsGiven" />
|
|
||||||
<label>Given</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary mt-3" type="submit">Save item</button>
|
|
||||||
</EditForm>
|
|
||||||
|
|
||||||
<table class="table table-striped mt-4">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Desired Qty</th>
|
|
||||||
<th>Participation</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var item in Items)
|
|
||||||
{
|
{
|
||||||
<tr>
|
<p class="mt-3">
|
||||||
<td>@item.Name</td>
|
<strong>Invite link:</strong>
|
||||||
<td>@item.DesiredQuantity</td>
|
<br />
|
||||||
<td>@(item.ParticipationAllowed ? "Yes" : "No")</td>
|
<a href="@InviteLink" target="_blank">@InviteLink</a>
|
||||||
<td>
|
</p>
|
||||||
<button class="btn btn-sm btn-outline-secondary me-2" @onclick="() => EditItem(item)">Edit</button>
|
|
||||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteItemAsync(item.Id)">Delete</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
}
|
||||||
</tbody>
|
</section>
|
||||||
</table>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ public partial class RegistryAdmin : ComponentBase
|
|||||||
protected string? InviteEmail { get; set; }
|
protected string? InviteEmail { get; set; }
|
||||||
protected string? InviteLink { get; private set; }
|
protected string? InviteLink { get; private set; }
|
||||||
protected BlazoredTextEditor? TextEditor { get; set; }
|
protected BlazoredTextEditor? TextEditor { get; set; }
|
||||||
|
protected string ActiveTab { get; set; } = "items";
|
||||||
|
protected RenderFragment ToolbarContent => builder =>
|
||||||
|
{
|
||||||
|
builder.AddMarkupContent(0, "<span class='ql-formats'><select class='ql-header'><option selected></option><option value='1'></option><option value='2'></option></select></span>");
|
||||||
|
builder.AddMarkupContent(1, "<span class='ql-formats'><button class='ql-bold'></button><button class='ql-italic'></button><button class='ql-underline'></button></span>");
|
||||||
|
builder.AddMarkupContent(2, "<span class='ql-formats'><button class='ql-list' value='ordered'></button><button class='ql-list' value='bullet'></button></span>");
|
||||||
|
builder.AddMarkupContent(3, "<span class='ql-formats'><button class='ql-link'></button><button class='ql-clean'></button></span>");
|
||||||
|
};
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
@@ -71,6 +79,21 @@ public partial class RegistryAdmin : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void SetActiveTab(string tab)
|
||||||
|
{
|
||||||
|
ActiveTab = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string GetTabClass(string tab)
|
||||||
|
{
|
||||||
|
return ActiveTab == tab ? "active" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string GetTabPaneClass(string tab)
|
||||||
|
{
|
||||||
|
return ActiveTab == tab ? "show active" : "";
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task SaveSettingsAsync()
|
protected async Task SaveSettingsAsync()
|
||||||
{
|
{
|
||||||
if (TextEditor is not null)
|
if (TextEditor is not null)
|
||||||
@@ -128,7 +151,7 @@ public partial class RegistryAdmin : ComponentBase
|
|||||||
DesiredQuantity = model.DesiredQuantity,
|
DesiredQuantity = model.DesiredQuantity,
|
||||||
ParticipationAllowed = model.ParticipationAllowed,
|
ParticipationAllowed = model.ParticipationAllowed,
|
||||||
ParticipationTargetAmount = model.ParticipationTargetAmount,
|
ParticipationTargetAmount = model.ParticipationTargetAmount,
|
||||||
CanBeSecondHand = model.CanBeSecondHand,
|
PreferSecondHand = model.PreferSecondHand,
|
||||||
IsGiven = model.IsGiven
|
IsGiven = model.IsGiven
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -195,6 +218,17 @@ public partial class RegistryAdmin : ComponentBase
|
|||||||
Admins = await RegistryService.GetRegistryAdminsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
Admins = await RegistryService.GetRegistryAdminsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async Task DeleteAdminAsync(string? userId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await RegistryService.RemoveRegistryAdminAsync(RegistryId, userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
Admins = await RegistryService.GetRegistryAdminsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadAsync()
|
private async Task LoadAsync()
|
||||||
{
|
{
|
||||||
var settings = await RegistryService.GetRegistrySettingsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
var settings = await RegistryService.GetRegistrySettingsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
||||||
@@ -209,6 +243,7 @@ public partial class RegistryAdmin : ComponentBase
|
|||||||
SettingsModel.BankAccountDisplayName = settings.BankAccountDisplayName;
|
SettingsModel.BankAccountDisplayName = settings.BankAccountDisplayName;
|
||||||
SettingsModel.BankAccountIban = settings.BankAccountIban;
|
SettingsModel.BankAccountIban = settings.BankAccountIban;
|
||||||
SettingsModel.BankAccountBic = settings.BankAccountBic;
|
SettingsModel.BankAccountBic = settings.BankAccountBic;
|
||||||
|
SettingsModel.ShowBankAccountName = settings.ShowBankAccountName;
|
||||||
}
|
}
|
||||||
|
|
||||||
Admins = await RegistryService.GetRegistryAdminsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
Admins = await RegistryService.GetRegistryAdminsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -1,3 +1,160 @@
|
|||||||
section {
|
section {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs styling */
|
||||||
|
.nav-tabs {
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link:hover {
|
||||||
|
border-bottom-color: #0d6efd;
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link.active {
|
||||||
|
border-bottom-color: #0d6efd;
|
||||||
|
color: #0d6efd;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
animation: fadeIn 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane.show.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor styling */
|
||||||
|
.editor-wrapper {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper ::deep .ql-container {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper ::deep .ql-editor {
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper ::deep .ql-toolbar {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper ::deep .ql-toolbar.ql-snow {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper ::deep .ql-snow .ql-picker-label {
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper ::deep .ql-snow .ql-stroke {
|
||||||
|
stroke: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper ::deep .ql-snow .ql-fill {
|
||||||
|
fill: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper ::deep .ql-snow .ql-picker.ql-expanded .ql-picker-item.selected {
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table styling */
|
||||||
|
.table {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-top: 2px solid #dee2e6;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form styling */
|
||||||
|
.form-check {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert styling */
|
||||||
|
.alert {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/registry/{Code}"
|
@page "/registry/{Code}"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
@using BirthList.Web.Features.Registries
|
@using BirthList.Web.Features.Registries
|
||||||
|
|
||||||
@@ -22,11 +23,11 @@ else
|
|||||||
{
|
{
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<strong>Shipping address</strong><br />
|
<strong>Shipping address</strong><br />
|
||||||
@Registry.ShippingAddress
|
<span class="shipping-address-text">@Registry.ShippingAddress</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Registry.BankAccountIban))
|
@if (Registry.ShowBankAccountName && !string.IsNullOrWhiteSpace(Registry.BankAccountIban))
|
||||||
{
|
{
|
||||||
<div class="alert alert-secondary">
|
<div class="alert alert-secondary">
|
||||||
<strong>Bank transfer participation</strong><br />
|
<strong>Bank transfer participation</strong><br />
|
||||||
@@ -42,16 +43,26 @@ else
|
|||||||
<div class="item-grid">
|
<div class="item-grid">
|
||||||
@foreach (var item in Registry.Items)
|
@foreach (var item in Registry.Items)
|
||||||
{
|
{
|
||||||
<article class="card item-card">
|
<article class="card item-card @(item.IsGiven ? "item-given" : "")">
|
||||||
@if (!string.IsNullOrWhiteSpace(item.PictureUrl))
|
@if (!string.IsNullOrWhiteSpace(item.PictureUrl))
|
||||||
{
|
{
|
||||||
<img class="card-img-top item-image" src="@item.PictureUrl" alt="@item.Name" />
|
<img class="card-img-top item-image" src="@item.PictureUrl" alt="@item.Name" />
|
||||||
}
|
}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">@item.Name</h5>
|
<div class="card-header-badges">
|
||||||
|
<h5 class="card-title">@item.Name</h5>
|
||||||
|
@if (item.PreferSecondHand == true)
|
||||||
|
{
|
||||||
|
<span class="badge bg-info">Second-hand preferred</span>
|
||||||
|
}
|
||||||
|
else if (item.PreferSecondHand == null)
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning">Second-hand optional</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@if (!string.IsNullOrWhiteSpace(item.Description))
|
@if (!string.IsNullOrWhiteSpace(item.Description))
|
||||||
{
|
{
|
||||||
<p class="card-text">@item.Description</p>
|
<p class="card-text item-description-text">@item.Description</p>
|
||||||
}
|
}
|
||||||
<p class="mb-1"><strong>Qty:</strong> @item.PurchasedQuantity/@item.DesiredQuantity purchased</p>
|
<p class="mb-1"><strong>Qty:</strong> @item.PurchasedQuantity/@item.DesiredQuantity purchased</p>
|
||||||
@if (item.PriceAmount.HasValue)
|
@if (item.PriceAmount.HasValue)
|
||||||
@@ -63,6 +74,54 @@ else
|
|||||||
<p class="mb-2"><strong>Participation:</strong> @item.MoneyFulfilledAmount.ToString("0.00") / @item.ParticipationTargetAmount.Value.ToString("0.00") @item.CurrencyCode</p>
|
<p class="mb-2"><strong>Participation:</strong> @item.MoneyFulfilledAmount.ToString("0.00") / @item.ParticipationTargetAmount.Value.ToString("0.00") @item.CurrencyCode</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (item.Purchasers.Count > 0 || item.Contributors.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="contributors-section mt-2 mb-2">
|
||||||
|
@if (item.Purchasers.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="mb-2">
|
||||||
|
@if (item.CanViewPurchasers && Registry.IsAdmin)
|
||||||
|
{
|
||||||
|
<strong class="text-sm">Purchased by:</strong>
|
||||||
|
<div class="contributor-list">
|
||||||
|
@foreach (var purchaser in item.Purchasers)
|
||||||
|
{
|
||||||
|
<span class="contributor-badge">@purchaser.DisplayName (@purchaser.Quantity)</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (item.CanViewPurchasers && item.CurrentUserPurchasedQuantity > 0)
|
||||||
|
{
|
||||||
|
<strong class="text-sm">Purchased by:</strong>
|
||||||
|
<div class="contributor-list">
|
||||||
|
<span class="contributor-badge">You (@item.CurrentUserPurchasedQuantity)</span>
|
||||||
|
@if (item.Purchasers.Count > 1)
|
||||||
|
{
|
||||||
|
<span class="contributor-badge">and @(item.Purchasers.Count - 1) other @(item.Purchasers.Count - 1 == 1 ? "person" : "people")</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-muted small">Purchased</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (item.Contributors.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong class="text-sm">Contributed by:</strong>
|
||||||
|
<div class="contributor-list">
|
||||||
|
@foreach (var contributor in item.Contributors)
|
||||||
|
{
|
||||||
|
<span class="contributor-badge">@contributor.DisplayName (@contributor.Amount.ToString("0.00") @item.CurrencyCode)</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (!IsAuthenticated)
|
@if (!IsAuthenticated)
|
||||||
{
|
{
|
||||||
<button class="btn btn-outline-primary btn-sm" @onclick="() => LoginRedirect()">Login to purchase</button>
|
<button class="btn btn-outline-primary btn-sm" @onclick="() => LoginRedirect()">Login to purchase</button>
|
||||||
@@ -70,11 +129,22 @@ else
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="d-flex gap-2 flex-wrap">
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
@if (!string.IsNullOrWhiteSpace(item.ProductUrl))
|
@if (!string.IsNullOrWhiteSpace(item.ProductUrl) && item.CurrentUserPurchasedQuantity == 0)
|
||||||
{
|
{
|
||||||
<a class="btn btn-primary btn-sm" href="@item.ProductUrl" target="_blank" @onclick="() => BeginPurchase(item.Id)">Purchase</a>
|
<button class="btn btn-primary btn-sm" @onclick="() => OpenPurchasePrompt(item.Id, openTab: true)">Purchase</button>
|
||||||
|
}
|
||||||
|
@if (item.CurrentUserPurchasedQuantity == 0)
|
||||||
|
{
|
||||||
|
<button class="btn btn-success btn-sm" @onclick="() => OpenPurchasePrompt(item.Id)">Mark purchased</button>
|
||||||
|
}
|
||||||
|
@if (item.CurrentUserPurchasedQuantity > 0 || (Registry.IsAdmin && item.Purchasers.Count > 0))
|
||||||
|
{
|
||||||
|
@if (item.CurrentUserPurchasedQuantity > 0)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success me-2">You purchased @item.CurrentUserPurchasedQuantity</span>
|
||||||
|
}
|
||||||
|
<button class="btn btn-warning btn-sm" @onclick="() => UnmarkPurchaseAsync(item.Id)">Unmark purchase</button>
|
||||||
}
|
}
|
||||||
<button class="btn btn-success btn-sm" @onclick="() => OpenPurchasePrompt(item.Id)">Mark purchased</button>
|
|
||||||
@if (item.ParticipationAllowed)
|
@if (item.ParticipationAllowed)
|
||||||
{
|
{
|
||||||
<button class="btn btn-secondary btn-sm" @onclick="() => OpenContributionPrompt(item.Id)">I transferred money</button>
|
<button class="btn btn-secondary btn-sm" @onclick="() => OpenContributionPrompt(item.Id)">I transferred money</button>
|
||||||
@@ -95,8 +165,16 @@ else
|
|||||||
<h3>Mark as purchased</h3>
|
<h3>Mark as purchased</h3>
|
||||||
<p>How many units did you purchase?</p>
|
<p>How many units did you purchase?</p>
|
||||||
<InputNumber class="form-control" @bind-Value="PurchasedQuantity" />
|
<InputNumber class="form-control" @bind-Value="PurchasedQuantity" />
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(PurchaseItemUrl))
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="@PurchaseItemUrl" target="_blank" class="btn btn-outline-primary btn-sm w-100">Open product link</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="d-flex gap-2 mt-3">
|
<div class="d-flex gap-2 mt-3">
|
||||||
<button class="btn btn-success" @onclick="ConfirmPurchaseAsync">Confirm</button>
|
<button class="btn btn-success" @onclick="ConfirmPurchaseAsync">Confirm purchase</button>
|
||||||
<button class="btn btn-outline-secondary" @onclick="ClosePurchasePrompt">Cancel</button>
|
<button class="btn btn-outline-secondary" @onclick="ClosePurchasePrompt">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,3 +196,26 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (ShowPurchaserSelectionPrompt)
|
||||||
|
{
|
||||||
|
<div class="prompt-overlay">
|
||||||
|
<div class="prompt-card">
|
||||||
|
<h3>Select purchaser to unmark</h3>
|
||||||
|
<p>Multiple users have purchased this item. Choose which purchase to unmark:</p>
|
||||||
|
<div class="purchaser-list">
|
||||||
|
@foreach (var purchaser in PurchasersToUnmark)
|
||||||
|
{
|
||||||
|
<div class="purchaser-item">
|
||||||
|
<span>@purchaser.DisplayName (@purchaser.Quantity)</span>
|
||||||
|
<button class="btn btn-warning btn-sm" @onclick="() => UnmarkPurchaserAsync(purchaser.UserId)">Unmark</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 mt-3">
|
||||||
|
<button class="btn btn-danger" @onclick="UnmarkAllPurchasersAsync">Unmark all</button>
|
||||||
|
<button class="btn btn-outline-secondary" @onclick="ClosePurchaserSelectionPrompt">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using BirthList.Domain.Entities;
|
||||||
using BirthList.Web.Features.Registries;
|
using BirthList.Web.Features.Registries;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
namespace BirthList.Web.Components.Pages;
|
namespace BirthList.Web.Components.Pages;
|
||||||
|
|
||||||
@@ -11,21 +13,30 @@ public partial class RegistryPublic : ComponentBase
|
|||||||
[Inject] private RegistryUserContext RegistryUserContext { get; set; } = null!;
|
[Inject] private RegistryUserContext RegistryUserContext { get; set; } = null!;
|
||||||
[Inject] private RegistryThemeService RegistryThemeService { get; set; } = null!;
|
[Inject] private RegistryThemeService RegistryThemeService { get; set; } = null!;
|
||||||
[Inject] private NavigationManager NavigationManager { get; set; } = null!;
|
[Inject] private NavigationManager NavigationManager { get; set; } = null!;
|
||||||
|
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
||||||
|
|
||||||
protected RegistryPublicViewModel? Registry { get; private set; }
|
protected RegistryPublicViewModel? Registry { get; private set; }
|
||||||
protected bool IsAuthenticated { get; private set; }
|
protected bool IsAuthenticated { get; private set; }
|
||||||
protected bool ShowPurchasePrompt { get; private set; }
|
protected bool ShowPurchasePrompt { get; private set; }
|
||||||
protected bool ShowContributionPrompt { get; private set; }
|
protected bool ShowContributionPrompt { get; private set; }
|
||||||
|
protected bool ShowPurchaserSelectionPrompt { get; private set; }
|
||||||
protected Guid ActiveItemId { get; private set; }
|
protected Guid ActiveItemId { get; private set; }
|
||||||
protected int PurchasedQuantity { get; set; } = 1;
|
protected int PurchasedQuantity { get; set; } = 1;
|
||||||
protected decimal ContributionAmount { get; set; }
|
protected decimal ContributionAmount { get; set; }
|
||||||
protected string ContributionMessage { get; set; } = string.Empty;
|
protected string ContributionMessage { get; set; } = string.Empty;
|
||||||
|
protected string? PurchaseItemUrl { get; private set; }
|
||||||
|
protected IReadOnlyList<ItemContributorViewModel> PurchasersToUnmark { get; private set; } = [];
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
IsAuthenticated = !string.IsNullOrWhiteSpace(userId);
|
IsAuthenticated = !string.IsNullOrWhiteSpace(userId);
|
||||||
Registry = await RegistryService.GetPublicRegistryByCodeAsync(Code, userId, CancellationToken.None).ConfigureAwait(false);
|
Registry = await RegistryService.GetPublicRegistryByCodeAsync(Code, userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (Registry is not null && !string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
await RegistryService.LogUserActionAsync(Registry.Id, userId, UserActionType.RegistryLinkOpened, null, null, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void LoginRedirect()
|
protected void LoginRedirect()
|
||||||
@@ -39,10 +50,23 @@ public partial class RegistryPublic : ComponentBase
|
|||||||
PurchasedQuantity = 1;
|
PurchasedQuantity = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void OpenPurchasePrompt(Guid itemId)
|
protected async Task OpenPurchasePrompt(Guid itemId, bool openTab = false)
|
||||||
{
|
{
|
||||||
|
var item = Registry?.Items.FirstOrDefault(x => x.Id == itemId);
|
||||||
|
if (item is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ActiveItemId = itemId;
|
ActiveItemId = itemId;
|
||||||
PurchasedQuantity = 1;
|
PurchasedQuantity = 1;
|
||||||
|
PurchaseItemUrl = item.ProductUrl;
|
||||||
|
|
||||||
|
if (openTab && !string.IsNullOrWhiteSpace(item.ProductUrl))
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("open", item.ProductUrl, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
ShowPurchasePrompt = true;
|
ShowPurchasePrompt = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +74,7 @@ public partial class RegistryPublic : ComponentBase
|
|||||||
{
|
{
|
||||||
ShowPurchasePrompt = false;
|
ShowPurchasePrompt = false;
|
||||||
ActiveItemId = Guid.Empty;
|
ActiveItemId = Guid.Empty;
|
||||||
|
PurchaseItemUrl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task ConfirmPurchaseAsync()
|
protected async Task ConfirmPurchaseAsync()
|
||||||
@@ -94,4 +119,69 @@ public partial class RegistryPublic : ComponentBase
|
|||||||
ShowContributionPrompt = false;
|
ShowContributionPrompt = false;
|
||||||
Registry = await RegistryService.GetPublicRegistryByCodeAsync(Code, userId, CancellationToken.None).ConfigureAwait(false);
|
Registry = await RegistryService.GetPublicRegistryByCodeAsync(Code, userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async Task UnmarkPurchaseAsync(Guid itemId)
|
||||||
|
{
|
||||||
|
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = Registry?.Items.FirstOrDefault(x => x.Id == itemId);
|
||||||
|
if (item is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If admin with multiple purchasers, show selection modal
|
||||||
|
if (Registry.IsAdmin)
|
||||||
|
{
|
||||||
|
ActiveItemId = itemId;
|
||||||
|
PurchasersToUnmark = item.Purchasers;
|
||||||
|
ShowPurchaserSelectionPrompt = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular user or single purchaser - unmark directly
|
||||||
|
await RegistryService.UnmarkPurchaseAsync(itemId, userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
Registry = await RegistryService.GetPublicRegistryByCodeAsync(Code, userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ClosePurchaserSelectionPrompt()
|
||||||
|
{
|
||||||
|
ShowPurchaserSelectionPrompt = false;
|
||||||
|
ActiveItemId = Guid.Empty;
|
||||||
|
PurchasersToUnmark = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task UnmarkPurchaserAsync(string purchaserUserId)
|
||||||
|
{
|
||||||
|
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await RegistryService.UnmarkPurchaseByAdminAsync(ActiveItemId, purchaserUserId, userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
ShowPurchaserSelectionPrompt = false;
|
||||||
|
Registry = await RegistryService.GetPublicRegistryByCodeAsync(Code, userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task UnmarkAllPurchasersAsync()
|
||||||
|
{
|
||||||
|
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var purchaser in PurchasersToUnmark)
|
||||||
|
{
|
||||||
|
await RegistryService.UnmarkPurchaseByAdminAsync(ActiveItemId, purchaser.UserId, userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowPurchaserSelectionPrompt = false;
|
||||||
|
Registry = await RegistryService.GetPublicRegistryByCodeAsync(Code, userId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item-image {
|
.item-image {
|
||||||
max-height: 220px;
|
width: 100%;
|
||||||
object-fit: cover;
|
aspect-ratio: 1 / 1;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: center;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-given {
|
||||||
|
opacity: 0.75;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-given .item-image {
|
||||||
|
background-color: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-given .card-body {
|
||||||
|
background-color: #b0b0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-given button {
|
||||||
|
opacity: 0.3;
|
||||||
|
/*background-color: #b0b0b0;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-given .btn-warning {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-overlay {
|
.prompt-overlay {
|
||||||
@@ -43,3 +68,76 @@
|
|||||||
.theme-birthday-default h1 {
|
.theme-birthday-default h1 {
|
||||||
color: #198754;
|
color: #198754;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shipping-address-text,
|
||||||
|
.item-description-text {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header-badges {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header-badges .card-title {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header-badges .badge {
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contributors-section {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contributor-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contributor-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchaser-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchaser-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchaser-item span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchaser-item .btn {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,8 +21,18 @@ public sealed class RegistryItemEditModel
|
|||||||
public int DesiredQuantity { get; set; } = 1;
|
public int DesiredQuantity { get; set; } = 1;
|
||||||
public bool ParticipationAllowed { get; set; }
|
public bool ParticipationAllowed { get; set; }
|
||||||
public decimal? ParticipationTargetAmount { get; set; }
|
public decimal? ParticipationTargetAmount { get; set; }
|
||||||
public bool CanBeSecondHand { get; set; }
|
public bool? PreferSecondHand { get; set; }
|
||||||
public bool IsGiven { 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
|
public sealed class RegistrySettingsEditModel
|
||||||
@@ -36,6 +46,7 @@ public sealed class RegistrySettingsEditModel
|
|||||||
public string? BankAccountIban { get; set; }
|
public string? BankAccountIban { get; set; }
|
||||||
public string? BankAccountBic { get; set; }
|
public string? BankAccountBic { get; set; }
|
||||||
public string? BankAccountDisplayName { get; set; }
|
public string? BankAccountDisplayName { get; set; }
|
||||||
|
public bool ShowBankAccountName { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class RegistrySummaryViewModel
|
public sealed class RegistrySummaryViewModel
|
||||||
@@ -61,6 +72,9 @@ public sealed class RegistryPublicViewModel
|
|||||||
public string? BankAccountIban { get; init; }
|
public string? BankAccountIban { get; init; }
|
||||||
public string? BankAccountBic { get; init; }
|
public string? BankAccountBic { get; init; }
|
||||||
public string? BankAccountDisplayName { 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; } = [];
|
public IReadOnlyList<RegistryPublicItemViewModel> Items { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,8 +92,12 @@ public sealed class RegistryPublicItemViewModel
|
|||||||
public bool ParticipationAllowed { get; init; }
|
public bool ParticipationAllowed { get; init; }
|
||||||
public decimal? ParticipationTargetAmount { get; init; }
|
public decimal? ParticipationTargetAmount { get; init; }
|
||||||
public decimal MoneyFulfilledAmount { get; init; }
|
public decimal MoneyFulfilledAmount { get; init; }
|
||||||
public bool CanBeSecondHand { get; init; }
|
public bool? PreferSecondHand { get; init; }
|
||||||
public bool IsGiven { 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
|
public sealed class RegistryAdminDisplayModel
|
||||||
@@ -97,3 +115,15 @@ public sealed class UrlMetadataResult
|
|||||||
public decimal? PriceAmount { get; init; }
|
public decimal? PriceAmount { get; init; }
|
||||||
public string? CurrencyCode { 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.Domain.Entities;
|
||||||
using BirthList.Infrastructure.Persistence;
|
using BirthList.Infrastructure.Persistence;
|
||||||
|
using BirthList.Web.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace BirthList.Web.Features.Registries;
|
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)
|
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)
|
public async Task<IReadOnlyList<RegistryAdminDisplayModel>> GetRegistryAdminsAsync(Guid registryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await registryDbContext.RegistryAdmins
|
var admins = await registryDbContext.RegistryAdmins
|
||||||
.Where(x => x.RegistryId == registryId)
|
.Where(x => x.RegistryId == registryId)
|
||||||
.OrderBy(x => x.AddedAtUtc)
|
.OrderBy(x => x.AddedAtUtc)
|
||||||
.Select(x => new RegistryAdminDisplayModel
|
.Select(x => x.UserId)
|
||||||
{
|
|
||||||
UserId = x.UserId,
|
|
||||||
DisplayName = x.UserId
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken)
|
.ToListAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.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)
|
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);
|
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
|
return new RegistryPublicViewModel
|
||||||
{
|
{
|
||||||
Id = registry.Id,
|
Id = registry.Id,
|
||||||
@@ -158,6 +209,9 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
|||||||
BankAccountIban = settings?.BankAccountIban,
|
BankAccountIban = settings?.BankAccountIban,
|
||||||
BankAccountBic = settings?.BankAccountBic,
|
BankAccountBic = settings?.BankAccountBic,
|
||||||
BankAccountDisplayName = settings?.BankAccountDisplayName,
|
BankAccountDisplayName = settings?.BankAccountDisplayName,
|
||||||
|
ShowBankAccountName = settings?.ShowBankAccountName ?? false,
|
||||||
|
CurrentUserId = userId,
|
||||||
|
IsAdmin = isAdmin,
|
||||||
Items = registry.Items
|
Items = registry.Items
|
||||||
.OrderBy(x => x.Name)
|
.OrderBy(x => x.Name)
|
||||||
.Select(x => new RegistryPublicItemViewModel
|
.Select(x => new RegistryPublicItemViewModel
|
||||||
@@ -174,8 +228,36 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
|||||||
ParticipationAllowed = x.ParticipationAllowed,
|
ParticipationAllowed = x.ParticipationAllowed,
|
||||||
ParticipationTargetAmount = x.ParticipationTargetAmount,
|
ParticipationTargetAmount = x.ParticipationTargetAmount,
|
||||||
MoneyFulfilledAmount = x.MoneyFulfilledAmount,
|
MoneyFulfilledAmount = x.MoneyFulfilledAmount,
|
||||||
CanBeSecondHand = x.CanBeSecondHand,
|
PreferSecondHand = x.PreferSecondHand,
|
||||||
IsGiven = x.IsGiven
|
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()
|
.ToList()
|
||||||
};
|
};
|
||||||
@@ -206,7 +288,8 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
|||||||
ThemeKey = registry.ThemeKey,
|
ThemeKey = registry.ThemeKey,
|
||||||
BankAccountIban = settings?.BankAccountIban,
|
BankAccountIban = settings?.BankAccountIban,
|
||||||
BankAccountBic = settings?.BankAccountBic,
|
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.BankAccountIban = string.IsNullOrWhiteSpace(model.BankAccountIban) ? null : model.BankAccountIban.Trim();
|
||||||
settings.BankAccountBic = string.IsNullOrWhiteSpace(model.BankAccountBic) ? null : model.BankAccountBic.Trim();
|
settings.BankAccountBic = string.IsNullOrWhiteSpace(model.BankAccountBic) ? null : model.BankAccountBic.Trim();
|
||||||
settings.BankAccountDisplayName = string.IsNullOrWhiteSpace(model.BankAccountDisplayName) ? null : model.BankAccountDisplayName.Trim();
|
settings.BankAccountDisplayName = string.IsNullOrWhiteSpace(model.BankAccountDisplayName) ? null : model.BankAccountDisplayName.Trim();
|
||||||
|
settings.ShowBankAccountName = model.ShowBankAccountName;
|
||||||
|
|
||||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<RegistryItemEditModel>> GetRegistryItemsAsync(Guid registryId, CancellationToken cancellationToken)
|
public async Task<IReadOnlyList<RegistryItemEditModel>> GetRegistryItemsAsync(Guid registryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await registryDbContext.RegistryItems
|
var items = await registryDbContext.RegistryItems
|
||||||
.Where(x => x.RegistryId == registryId)
|
.Where(x => x.RegistryId == registryId)
|
||||||
.OrderBy(x => x.Name)
|
.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
|
.Select(x => new RegistryItemEditModel
|
||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
@@ -262,34 +386,104 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
|||||||
DesiredQuantity = x.DesiredQuantity,
|
DesiredQuantity = x.DesiredQuantity,
|
||||||
ParticipationAllowed = x.ParticipationAllowed,
|
ParticipationAllowed = x.ParticipationAllowed,
|
||||||
ParticipationTargetAmount = x.ParticipationTargetAmount,
|
ParticipationTargetAmount = x.ParticipationTargetAmount,
|
||||||
CanBeSecondHand = x.CanBeSecondHand,
|
PreferSecondHand = x.PreferSecondHand,
|
||||||
IsGiven = x.IsGiven
|
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)
|
.ToList();
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RegistryItemEditModel?> GetRegistryItemAsync(Guid registryId, Guid itemId, CancellationToken cancellationToken)
|
public async Task<RegistryItemEditModel?> GetRegistryItemAsync(Guid registryId, Guid itemId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await registryDbContext.RegistryItems
|
var item = await registryDbContext.RegistryItems
|
||||||
.Where(x => x.RegistryId == registryId && x.Id == itemId)
|
.FirstOrDefaultAsync(x => x.RegistryId == registryId && x.Id == itemId, cancellationToken)
|
||||||
.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)
|
|
||||||
.ConfigureAwait(false);
|
.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)
|
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.DesiredQuantity = model.DesiredQuantity < 1 ? 1 : model.DesiredQuantity;
|
||||||
entity.ParticipationAllowed = model.ParticipationAllowed;
|
entity.ParticipationAllowed = model.ParticipationAllowed;
|
||||||
entity.ParticipationTargetAmount = model.ParticipationAllowed ? model.ParticipationTargetAmount : null;
|
entity.ParticipationTargetAmount = model.ParticipationAllowed ? model.ParticipationTargetAmount : null;
|
||||||
entity.CanBeSecondHand = model.CanBeSecondHand;
|
entity.PreferSecondHand = model.PreferSecondHand;
|
||||||
entity.IsGiven = model.IsGiven;
|
entity.IsGiven = model.IsGiven;
|
||||||
|
|
||||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -370,6 +564,17 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
|||||||
item.PurchasedQuantity += normalizedQuantity;
|
item.PurchasedQuantity += normalizedQuantity;
|
||||||
item.IsGiven = item.PurchasedQuantity >= item.DesiredQuantity;
|
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);
|
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +602,153 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
|||||||
});
|
});
|
||||||
|
|
||||||
item.MoneyFulfilledAmount += amount;
|
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);
|
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,6 +810,21 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext)
|
|||||||
return true;
|
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)
|
private async Task<string> CreateUniquePublicCodeAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
for (var attempt = 0; attempt < 10; attempt++)
|
for (var attempt = 0; attempt < 10; attempt++)
|
||||||
|
|||||||
Reference in New Issue
Block a user