196 lines
7.7 KiB
Plaintext
196 lines
7.7 KiB
Plaintext
@page "/admin/products"
|
|
@layout AdminLayout
|
|
@using Generic
|
|
|
|
@inject ProductAPIConsumer productAPIConsumer
|
|
@inject ISnackbar Snackbar
|
|
@inject IJSRuntime JS
|
|
|
|
<div class="flex flex-col h-full w-full gap-5 p-6">
|
|
|
|
<div class="flex flex-row justify-between items-center">
|
|
<h1 class="text-2xl font-bold">Manage Products</h1>
|
|
<a class="btn btn-primary" href="/manage-product">Add Product</a>
|
|
</div>
|
|
|
|
<div class="bg-base-200 p-4 rounded-lg">
|
|
<div class="flex flex-col sm:flex-row gap-4 items-end">
|
|
<div class="form-control w-full max-w-xs">
|
|
<label class="label"><span class="label-text">Search Products</span></label>
|
|
<div class="relative">
|
|
<input type="text" placeholder="Search by name..." class="input input-bordered w-full pr-10"
|
|
@bind="productFilter.Name" @bind:event="oninput" @onkeyup="HandleKeyUp" />
|
|
<button type="button" class="absolute right-0 top-0 h-full px-3 flex items-center justify-center"
|
|
@onclick="LoadProducts">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-50" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn btn-ghost" @onclick="ClearFilters">Clear</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto bg-base-100 rounded-lg shadow">
|
|
<table class="table w-full">
|
|
<thead>
|
|
<tr>
|
|
<th class="w-16">Pic</th>
|
|
<th>Name</th>
|
|
<th>Price</th>
|
|
<th class="hidden md:table-cell">Description</th>
|
|
<th class="text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (_loading)
|
|
{
|
|
<tr>
|
|
<td colspan="5" class="text-center py-10">
|
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
</td>
|
|
</tr>
|
|
}
|
|
else if (products == null || !products.Any())
|
|
{
|
|
<tr>
|
|
<td colspan="5" class="text-center py-10 opacity-50">No products found.</td>
|
|
</tr>
|
|
}
|
|
else
|
|
{
|
|
@foreach (var product in products)
|
|
{
|
|
<tr class="hover">
|
|
<td>
|
|
<div class="avatar">
|
|
<div class="mask mask-squircle w-12 h-12 bg-base-200">
|
|
@if (product.Photos != null && product.Photos.Any())
|
|
{
|
|
<img src="@product.Photos.First()" alt="Product image"
|
|
class="object-fill h-full w-full" />
|
|
}
|
|
else
|
|
{
|
|
<div class="flex items-center justify-center h-full w-full opacity-30">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="font-medium text-primary">@product.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</td>
|
|
<td>@product.Price?.ToString("C")</td>
|
|
<td class="hidden md:table-cell max-w-xs truncate">@product.Translations?.FirstOrDefault()?.Info?.Description ?? string.Empty)</td>
|
|
<td class="text-right flex justify-end gap-2">
|
|
<a class="btn btn-ghost btn-xs btn-primary" href="@($"/manage-product/{product.Id}")">
|
|
Edit
|
|
</a>
|
|
<button class="btn btn-ghost btn-xs btn-error" @onclick="@(() => Remove(product))">
|
|
Delete
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
}
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<PaginationComp PaginationModel="@pagination" OnSearch="HandlePageChange" />
|
|
</div>
|
|
|
|
</div>
|
|
|
|
@code {
|
|
ProductFilter productFilter = new();
|
|
Pagination pagination = new() { PageSize = 10, CurrentPage = 1 };
|
|
bool _loading = false;
|
|
|
|
List<ProductVM> products = new();
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadProducts();
|
|
}
|
|
|
|
async Task LoadProducts()
|
|
{
|
|
_loading = true;
|
|
|
|
try
|
|
{
|
|
var result = await productAPIConsumer.FetchProduct(productFilter, pagination);
|
|
products = result.Item1 ?? new List<ProductVM>();
|
|
pagination.ItemsCount = result.Item2;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error loading products: {ex.Message}");
|
|
products = new List<ProductVM>();
|
|
ToastService.ShowToast("Failed to load products", ToastLevel.Error);
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
|
|
async Task HandlePageChange(int page)
|
|
{
|
|
pagination.CurrentPage = page;
|
|
await LoadProducts();
|
|
}
|
|
|
|
async Task HandleKeyUp(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Enter")
|
|
{
|
|
pagination.CurrentPage = 1;
|
|
await LoadProducts();
|
|
}
|
|
}
|
|
|
|
async Task ClearFilters()
|
|
{
|
|
productFilter = new();
|
|
pagination.CurrentPage = 1;
|
|
await LoadProducts();
|
|
}
|
|
|
|
async Task Remove(ProductVM product)
|
|
{
|
|
var confirmed = await JS.InvokeAsync<bool>("confirm", $"Are you sure you want to delete '{product.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty}'?");
|
|
|
|
if (confirmed)
|
|
{
|
|
if (product.Id == null) return;
|
|
try
|
|
{
|
|
var isSuccess = await productAPIConsumer.DeleteProduct(product.Id.Value.ToString());
|
|
if (isSuccess)
|
|
{
|
|
ToastService.ShowToast("Product deleted successfully", ToastLevel.Success);
|
|
await LoadProducts(); // Reload to refresh list and pagination
|
|
}
|
|
else
|
|
{
|
|
ToastService.ShowToast("Failed to delete product", ToastLevel.Error);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ToastService.ShowToast($"Error: {ex.Message}", ToastLevel.Error);
|
|
}
|
|
}
|
|
}
|
|
} |