Add UserActionLog and enhance registry features
Build and Push Docker Image / build-and-push (push) Successful in 1m59s

- Introduced `UserActionLog` entity to track user actions.
- Replaced `CanBeSecondHand` with `PreferSecondHand` property.
- Added `ShowBankAccountName` to `RegistrySettings`.
- Updated models and migrations for new properties.
- Enhanced `RegistryService` with user action logging and item details.
- Redesigned `Home.razor` with a grid layout and modal for registries.
- Added `RegistryActionLog.razor` for admin action logs.
- Improved `RegistryPublic.razor` with purchaser/contributor details.
- Replaced sidebar with `TopBar.razor` for responsive navigation.
- Updated CSS for new components and improved responsiveness.
This commit is contained in:
Arne Moerman
2026-05-17 20:57:54 +02:00
parent 6b593828d7
commit 2bf0295508
32 changed files with 3811 additions and 435 deletions
@@ -20,172 +20,289 @@ else
</div>
}
<section class="mb-4">
<h2>Current admins</h2>
@if (Admins.Count == 0)
{
<p>No admins assigned yet.</p>
}
else
{
<ul>
@foreach (var admin in Admins)
{
<li>@admin.DisplayName</li>
}
</ul>
}
</section>
<ul class="nav nav-tabs mb-4" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link @(GetTabClass("items"))"
id="items-tab"
@onclick='() => SetActiveTab("items")'
type="button"
role="tab"
aria-controls="items-content"
aria-selected="@(ActiveTab == "items" ? "true" : "false")">
<span class="bi bi-box-seam"></span> Items
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(GetTabClass("settings"))"
id="settings-tab"
@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">
<h2>Settings</h2>
<EditForm Model="SettingsModel" OnValidSubmit="SaveSettingsAsync" FormName="registry-settings-form">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Baby name</label>
<InputText class="form-control" @bind-Value="SettingsModel.BabyName" />
</div>
<div class="col-md-3">
<label class="form-label">Birth date</label>
<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="tab-content">
<!-- Items Tab -->
<div class="tab-pane fade @(GetTabPaneClass("items"))" id="items-content" role="tabpanel" aria-labelledby="items-tab">
<section class="mb-4">
<h2>Add or edit item</h2>
<EditForm Model="ItemModel" OnValidSubmit="SaveItemAsync" FormName="registry-item-form">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Name</label>
<InputText class="form-control" @bind-Value="ItemModel.Name" />
</div>
<div class="col-md-6">
<label class="form-label">Product URL</label>
<div class="input-group">
<InputText class="form-control" @bind-Value="ItemModel.ProductUrl" />
<button type="button" class="btn btn-outline-secondary" @onclick="FetchMetadataAsync">Auto fetch</button>
</div>
</div>
<div class="col-md-6">
<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">
<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">
<label class="form-label">Shipping address</label>
<InputTextArea class="form-control" @bind-Value="SettingsModel.ShippingAddress" rows="3" />
</div>
<div class="mt-3">
<label class="form-label">Top content</label>
<BlazoredTextEditor @ref="TextEditor" Placeholder="Welcome text" Theme="snow" />
</div>
<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>
<button class="btn btn-primary mt-3" type="submit">Save settings</button>
</EditForm>
</section>
<section class="mb-4">
<h2>Invite admin</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>
<section>
<h2>Items</h2>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Desired Qty</th>
<th>Participation</th>
<th>Purchased by / Contributed by</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Items)
{
<tr>
<td>@item.Name</td>
<td>@item.DesiredQuantity</td>
<td>@(item.ParticipationAllowed ? "Yes" : "No")</td>
<td>
@if (item.Purchasers.Count > 0)
{
<div class="mb-1">
<small><strong>Purchased:</strong></small>
<div class="small">
@foreach (var purchaser in item.Purchasers)
{
<div>@purchaser.DisplayName (@purchaser.Quantity)</div>
}
</div>
</div>
}
@if (item.Contributors.Count > 0)
{
<div>
<small><strong>Contributed:</strong></small>
<div class="small">
@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>
@if (!string.IsNullOrWhiteSpace(InviteLink))
{
<p class="mt-2">Invite link: <a href="@InviteLink">@InviteLink</a></p>
}
</section>
<section>
<h2>Add or edit item</h2>
<EditForm Model="ItemModel" OnValidSubmit="SaveItemAsync" FormName="registry-item-form">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Name</label>
<InputText class="form-control" @bind-Value="ItemModel.Name" />
</div>
<div class="col-md-6">
<label class="form-label">Product URL</label>
<div class="input-group">
<InputText class="form-control" @bind-Value="ItemModel.ProductUrl" />
<button type="button" class="btn btn-outline-secondary" @onclick="FetchMetadataAsync">Auto fetch</button>
<!-- Settings Tab -->
<div class="tab-pane fade @(GetTabPaneClass("settings"))" id="settings-content" role="tabpanel" aria-labelledby="settings-tab">
<section class="mb-4">
<h2>Registry Settings</h2>
<EditForm Model="SettingsModel" OnValidSubmit="SaveSettingsAsync" FormName="registry-settings-form">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Baby name</label>
<InputText class="form-control" @bind-Value="SettingsModel.BabyName" />
</div>
<div class="col-md-3">
<label class="form-label">Birth date</label>
<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 class="col-md-6">
<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)
@if (!string.IsNullOrWhiteSpace(InviteLink))
{
<tr>
<td>@item.Name</td>
<td>@item.DesiredQuantity</td>
<td>@(item.ParticipationAllowed ? "Yes" : "No")</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>
<p class="mt-3">
<strong>Invite link:</strong>
<br />
<a href="@InviteLink" target="_blank">@InviteLink</a>
</p>
}
</tbody>
</table>
</section>
</section>
</div>
</div>
}