566 lines
31 KiB
Plaintext
566 lines
31 KiB
Plaintext
@page "/manage-product/{id?}"
|
|
@layout AdminLayout
|
|
@using Generic
|
|
@using Commerce.Contracts.DTOs
|
|
@inject ProductAPIConsumer productAPIConsumer
|
|
@inject CategoryAPIConsumer categoryAPIConsumer
|
|
@inject IToastService ToastService
|
|
|
|
<div class="max-w-7xl mx-auto mt-10 px-4 pb-32 min-h-screen">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<h3 class="text-3xl font-extrabold text-base-content tracking-tight">
|
|
@(string.IsNullOrEmpty(Id) ? "Add New Product" : "Edit Product")
|
|
</h3>
|
|
|
|
<!-- Global Language Switcher -->
|
|
<div class="flex gap-1 bg-base-200 p-1.5 rounded-2xl border border-base-300 shadow-sm">
|
|
@foreach (var lang in _languages)
|
|
{
|
|
<button type="button" @onclick="() => _activeLang = lang.Key"
|
|
class="btn btn-sm border-none transition-all duration-300 rounded-xl @(_activeLang == lang.Key ? "btn-primary shadow-md" : "btn-ghost opacity-60")">
|
|
@lang.Value
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<EditForm EditContext="_editContext" OnSubmit="HandleSubmit">
|
|
<FluentValidationValidator />
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-start">
|
|
<!-- SIDEBAR: Generic Properties -->
|
|
<div class="lg:col-span-1 space-y-8">
|
|
<!-- Configuration Card -->
|
|
<div class="card bg-base-100 shadow-xl border border-base-200 !overflow-visible">
|
|
<div class="p-6 space-y-6">
|
|
<div class="flex items-center gap-2 pb-2 border-b border-base-200">
|
|
<div class="p-2 bg-primary/10 rounded-lg text-primary">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
</div>
|
|
<h4 class="font-bold text-lg">Product Settings</h4>
|
|
</div>
|
|
|
|
<!-- Code Name -->
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-semibold">Code Name</span></label>
|
|
<InputText @bind-value="product.CodeName"
|
|
class="input input-bordered w-full focus:input-primary transition-all"
|
|
placeholder="PROD-XXX" />
|
|
<ValidationMessage For="@(() => product.CodeName)" class="text-error text-xs mt-1" />
|
|
</div>
|
|
|
|
<!-- Price & Discount -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-semibold">Price ($)</span></label>
|
|
<InputNumber @bind-value="product.Price"
|
|
class="input input-bordered w-full focus:input-primary transition-all" />
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-semibold">Discount (%)</span></label>
|
|
<InputNumber @bind-value="product.Discount"
|
|
class="input input-bordered w-full focus:input-primary transition-all" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Category -->
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-semibold">Category</span></label>
|
|
<CategoryTreeSelect @bind-value="product.CategoryId" Placeholder="Select Category" />
|
|
</div>
|
|
|
|
<!-- Bundle Toggle -->
|
|
<div class="form-control p-4 bg-base-200/50 rounded-2xl border border-base-300">
|
|
<label class="label cursor-pointer justify-between">
|
|
<span class="label-text font-bold">Bundle Product?</span>
|
|
<InputCheckbox @bind-value="product.IsBundle" class="toggle toggle-primary" />
|
|
</label>
|
|
<p class="text-[10px] text-base-content/50 mt-1 uppercase tracking-widest font-bold">Enable
|
|
to compose from other products</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
<!-- MAIN: Localized Content -->
|
|
<div class="lg:col-span-2 space-y-8">
|
|
<!-- Localized Content Card -->
|
|
<div class="card bg-base-100 shadow-2xl border border-base-200 !overflow-visible">
|
|
<div class="p-8 space-y-10">
|
|
<!-- Product Name -->
|
|
<div class="form-control">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h4 class="text-xl font-black tracking-tight flex items-center gap-2">
|
|
Product Name
|
|
<div class="badge badge-primary badge-sm">@(_languages[_activeLang])</div>
|
|
</h4>
|
|
</div>
|
|
<MultiLangInput @bind="_names" ActiveCulture="@_activeLang" HideTabs="true"
|
|
Placeholder="Enter product name in the selected language..." />
|
|
<ValidationMessage For="@(() => _names)" class="text-error text-sm mt-1" />
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="form-control">
|
|
<h4 class="text-xl font-black tracking-tight mb-4 flex items-center gap-2">
|
|
Description
|
|
<div class="badge badge-secondary badge-sm">@(_languages[_activeLang])</div>
|
|
</h4>
|
|
<MultiLangInput @bind="_descriptions" ActiveCulture="@_activeLang" HideTabs="true"
|
|
IsTextArea="true" Placeholder="Write a compelling product description..." />
|
|
<ValidationMessage For="@(() => _descriptions)" class="text-error text-sm mt-1" />
|
|
</div>
|
|
|
|
<!-- Technical Details / Specifications -->
|
|
<div class="form-control pt-6 border-t border-base-200">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h4 class="text-xl font-black tracking-tight flex items-center gap-2">
|
|
Product Specifications
|
|
<div class="badge badge-accent badge-sm">@(_languages[_activeLang])</div>
|
|
</h4>
|
|
<button type="button" @onclick="AddTechnicalDetail"
|
|
class="btn btn-sm btn-primary rounded-xl gap-2 shadow-lg shadow-primary/20">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
Add Spec
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
@if (!_technicalDetails.ContainsKey(_activeLang) ||
|
|
!_technicalDetails[_activeLang].Any())
|
|
{
|
|
<div
|
|
class="border-2 border-dashed border-base-200 rounded-3xl p-10 text-center opacity-30">
|
|
<p class="text-sm font-bold uppercase tracking-widest">No specifications for
|
|
@(_languages[_activeLang])</p>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
@foreach (var item in _technicalDetails[_activeLang])
|
|
{
|
|
<div
|
|
class="grid grid-cols-1 md:grid-cols-2 gap-4 bg-base-200/30 p-4 rounded-2xl border border-base-300 relative group transition-all hover:bg-base-200/50">
|
|
<div class="form-control">
|
|
<label class="label p-0 mb-1">
|
|
<span
|
|
class="text-[9px] uppercase font-black text-primary/60">Attribute</span>
|
|
</label>
|
|
<InputText @bind-value="item.Key"
|
|
class="input input-bordered input-sm rounded-xl focus:input-primary"
|
|
Placeholder="e.g. Weight" />
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label p-0 mb-1">
|
|
<span class="text-[9px] uppercase font-black text-accent/60">Value</span>
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<div class="flex-1">
|
|
<InputText @bind-value="item.Value"
|
|
class="input input-bordered input-sm rounded-xl focus:input-primary w-full"
|
|
Placeholder="e.g. 1.2kg" />
|
|
</div>
|
|
<button type="button" @onclick="() => RemoveTechnicalDetail(item)"
|
|
class="btn btn-ghost btn-circle btn-sm text-error self-end">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Media Card -->
|
|
<div class="card bg-base-100 shadow-xl border border-base-200 !overflow-visible">
|
|
<div class="p-8">
|
|
<div class="flex items-center gap-2 mb-6">
|
|
<div class="p-2 bg-secondary/10 rounded-lg text-secondary">
|
|
<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>
|
|
<h4 class="font-bold text-xl">Product Media</h4>
|
|
</div>
|
|
<Generic.Media.Components.Media @ref="mediaComponent" InitialUrls="@(product.Photos?.ToList() ?? new List<string>())"
|
|
SingleUploadMode="false" MaxItems="10" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bundle Composition Section (Conditional) -->
|
|
@if (product.IsBundle)
|
|
{
|
|
<div class="card bg-base-100 shadow-xl border border-primary/20 !overflow-visible">
|
|
<div class="bg-primary/5 p-6 border-b border-primary/10">
|
|
<h4 class="text-xl font-bold text-primary flex items-center gap-3">
|
|
<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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
</svg>
|
|
Bundle Composition
|
|
</h4>
|
|
</div>
|
|
<div class="p-8 space-y-8">
|
|
<!-- Selected Products -->
|
|
<div class="space-y-4">
|
|
<p class="text-sm font-bold text-base-content/40 uppercase tracking-widest">Included
|
|
Products (@(product.ChildProducts?.Count ?? 0))</p>
|
|
@if (product.ChildProducts == null || !product.ChildProducts.Any())
|
|
{
|
|
<div
|
|
class="border-2 border-dashed border-base-300 rounded-3xl p-12 text-center text-base-content/40 flex flex-col items-center gap-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 opacity-20" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
|
</svg>
|
|
<span class="italic font-medium">No products added. Search and add below.</span>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
@foreach (var item in product.ChildProducts)
|
|
{
|
|
<div
|
|
class="flex items-center justify-between bg-base-100 p-4 rounded-2xl shadow-sm border border-base-200 group hover:border-primary/50 hover:shadow-lg transition-all">
|
|
<div class="flex items-center gap-4">
|
|
<div
|
|
class="w-12 h-12 rounded-xl bg-base-200 flex-shrink-0 overflow-hidden ring-1 ring-base-300">
|
|
@if (item.Photos?.Any() == true)
|
|
{
|
|
<img src="@item.Photos[0]" class="w-full h-full object-cover" />
|
|
}
|
|
</div>
|
|
<div class="flex flex-col">
|
|
<span
|
|
class="font-bold text-sm leading-tight">@item.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</span>
|
|
<span
|
|
class="text-xs font-bold text-primary">@item.Price?.ToString("C")</span>
|
|
</div>
|
|
</div>
|
|
<button type="button" @onclick="() => RemoveFromBundle(item)"
|
|
class="btn btn-ghost btn-circle btn-sm text-error opacity-0 group-hover:opacity-100 transition-all">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Finder -->
|
|
<div class="space-y-4 pt-6 border-t border-base-200">
|
|
<p class="text-sm font-bold text-base-content/40 uppercase tracking-widest">Find & Add
|
|
Products</p>
|
|
<div class="flex flex-col sm:flex-row gap-3">
|
|
<div class="relative flex-1">
|
|
<input type="text" @bind="searchFilter.Name" @bind:event="oninput"
|
|
@onkeyup="HandleSearchKeyUp" placeholder="Search by name..."
|
|
class="input input-bordered w-full pr-12 rounded-2xl focus:input-primary transition-all" />
|
|
<div class="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none">
|
|
@if (_isSearching)
|
|
{
|
|
<span class="loading loading-spinner loading-xs text-primary"></span>
|
|
}
|
|
else
|
|
{
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-20" 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>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="sm:w-80">
|
|
<CategoryTreeSelect @bind-value="searchFilter.CategoryId"
|
|
@bind-value:after="SearchProducts" ShowBundles="false"
|
|
Placeholder="All Categories" />
|
|
</div>
|
|
</div>
|
|
|
|
@if (_searchResults != null && _searchResults.Any())
|
|
{
|
|
<div
|
|
class="bg-base-200/50 rounded-3xl border border-base-300 p-2 max-h-72 overflow-y-auto custom-grid-scrollbar shadow-inner mt-4">
|
|
@foreach (var res in _searchResults)
|
|
{
|
|
<div class="flex items-center justify-between p-3 rounded-2xl hover:bg-base-100 transition-all cursor-pointer group"
|
|
@onclick="() => AddToBundle(res)">
|
|
<div class="flex items-center gap-4">
|
|
<div
|
|
class="w-10 h-10 rounded-xl bg-base-300 flex-shrink-0 border border-base-300">
|
|
@if (res.Photos?.Any() == true)
|
|
{
|
|
<img src="@res.Photos[0]" class="w-full h-full object-cover rounded-xl" />
|
|
}
|
|
</div>
|
|
<div class="flex flex-col">
|
|
<span
|
|
class="text-sm font-bold leading-none mb-1">@res.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</span>
|
|
<span class="text-xs opacity-50">@res.Price?.ToString("C")</span>
|
|
</div>
|
|
</div>
|
|
<button type="button"
|
|
class="btn btn-circle btn-sm btn-ghost group-hover:bg-primary group-hover:text-primary-content transition-all">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Action Bar -->
|
|
<div
|
|
class="flex items-center justify-end gap-4 p-8 bg-base-300/30 rounded-3xl border border-base-300 shadow-inner">
|
|
<button type="button" @onclick="ResetForm" class="btn btn-ghost px-8 rounded-2xl">Reset</button>
|
|
<button type="submit"
|
|
class="btn btn-primary btn-wide rounded-2xl shadow-xl shadow-primary/20 gap-3">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
|
</svg>
|
|
Save Product Changes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</EditForm>
|
|
</div>
|
|
|
|
@code {
|
|
|
|
[Parameter]
|
|
public string? Id { get; set; }
|
|
|
|
ProductVM product = new();
|
|
private EditContext? _editContext;
|
|
Media? mediaComponent;
|
|
|
|
// Search state
|
|
ProductFilter searchFilter = new() { IsBundle = false };
|
|
Dictionary<string, string> _names = new();
|
|
Dictionary<string, string> _descriptions = new();
|
|
Dictionary<string, List<TechnicalDetailVM>> _technicalDetails = new();
|
|
|
|
List<ProductVM> _searchResults = new();
|
|
bool _isSearching = false;
|
|
|
|
private string _activeLang = "en";
|
|
private readonly Dictionary<string, string> _languages = new()
|
|
{
|
|
{ "en", "English" },
|
|
{ "ar", "العربية" },
|
|
{ "es", "Español" },
|
|
{ "de", "Deutsch" }
|
|
};
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
if (!string.IsNullOrEmpty(Id))
|
|
{
|
|
product = await productAPIConsumer.GetProduct(Id);
|
|
}
|
|
|
|
// Map from Translations to local dictionaries
|
|
foreach (var t in product.Translations ?? new())
|
|
{
|
|
_names[t.Language] = t.Info.Name;
|
|
_descriptions[t.Language] = t.Info.Description;
|
|
_technicalDetails[t.Language] = t.Info.TechnicalDetails?.ToList() ?? new();
|
|
}
|
|
|
|
// Ensure supported cultures exist in dictionaries
|
|
foreach (var lang in _languages.Keys)
|
|
{
|
|
if (!_names.ContainsKey(lang)) _names[lang] = "";
|
|
if (!_descriptions.ContainsKey(lang)) _descriptions[lang] = "";
|
|
if (!_technicalDetails.ContainsKey(lang)) _technicalDetails[lang] = new();
|
|
}
|
|
|
|
product.Photos ??= Array.Empty<string>();
|
|
product.ChildProducts ??= new List<ProductVM>();
|
|
|
|
SyncTranslations();
|
|
_editContext = new EditContext(product);
|
|
}
|
|
|
|
void SyncTranslations()
|
|
{
|
|
product.Translations = _languages.Keys.Select(lang => new ProductTranslationVM
|
|
{
|
|
Language = lang,
|
|
Info = new ProductInfoVM
|
|
{
|
|
Name = _names.GetValueOrDefault(lang, ""),
|
|
Description = _descriptions.GetValueOrDefault(lang, ""),
|
|
TechnicalDetails = _technicalDetails.GetValueOrDefault(lang, new())
|
|
}
|
|
}).ToList();
|
|
}
|
|
|
|
async Task HandleSubmit()
|
|
{
|
|
SyncTranslations();
|
|
if (_editContext != null && _editContext.Validate())
|
|
{
|
|
await OnSubmit();
|
|
}
|
|
}
|
|
|
|
|
|
void AddTechnicalDetail()
|
|
{
|
|
if (!_technicalDetails.ContainsKey(_activeLang))
|
|
{
|
|
_technicalDetails[_activeLang] = new List<TechnicalDetailVM>();
|
|
}
|
|
_technicalDetails[_activeLang].Add(new TechnicalDetailVM());
|
|
}
|
|
|
|
void RemoveTechnicalDetail(TechnicalDetailVM item)
|
|
{
|
|
if (_technicalDetails.ContainsKey(_activeLang))
|
|
{
|
|
_technicalDetails[_activeLang].Remove(item);
|
|
}
|
|
}
|
|
|
|
async Task HandleSearchKeyUp()
|
|
{
|
|
await SearchProducts();
|
|
}
|
|
|
|
async Task SearchProducts()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(searchFilter.Name) && !searchFilter.CategoryId.HasValue)
|
|
{
|
|
_searchResults.Clear();
|
|
return;
|
|
}
|
|
|
|
_isSearching = true;
|
|
try
|
|
{
|
|
var (results, _) = await productAPIConsumer.FetchProduct(searchFilter, new Pagination { PageSize = 10, CurrentPage = 1 });
|
|
// Filter out the current product itself to avoid circular bundles if it already exists
|
|
_searchResults = results.Where(x => x.Id != product.Id).ToList();
|
|
}
|
|
finally
|
|
{
|
|
_isSearching = false;
|
|
}
|
|
}
|
|
|
|
void AddToBundle(ProductVM item)
|
|
{
|
|
if (product.ChildProducts == null) product.ChildProducts = new List<ProductVM>();
|
|
if (!product.ChildProducts.Any(x => x.Id == item.Id))
|
|
{
|
|
product.ChildProducts.Add(item);
|
|
_searchResults.Clear();
|
|
searchFilter.Name = "";
|
|
}
|
|
}
|
|
|
|
void RemoveFromBundle(ProductVM item)
|
|
{
|
|
if (product.ChildProducts != null)
|
|
{
|
|
product.ChildProducts.Remove(item);
|
|
}
|
|
}
|
|
|
|
async Task OnSubmit()
|
|
{
|
|
Console.WriteLine("OnSubmit started");
|
|
try
|
|
{
|
|
if (mediaComponent != null)
|
|
{
|
|
Console.WriteLine("Starting ProcessUploadsAsync");
|
|
var uploadResult = await mediaComponent.ProcessUploadsAsync();
|
|
Console.WriteLine("ProcessUploadsAsync finished successfully");
|
|
product.Photos = uploadResult.FinalUrls.ToArray();
|
|
}
|
|
|
|
SyncTranslations();
|
|
|
|
if (string.IsNullOrEmpty(Id))
|
|
{
|
|
Console.WriteLine("Creating product...");
|
|
await productAPIConsumer.CreateProduct(product);
|
|
ToastService.ShowToast("Product added successfully!", ToastLevel.Success);
|
|
ResetForm();
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Updating product...");
|
|
await productAPIConsumer.UpdateProduct(product);
|
|
ToastService.ShowToast("Product updated successfully!", ToastLevel.Success);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Caught exception in OnSubmit: {ex.Message}");
|
|
ToastService.ShowToast($"Error: {ex.Message}", ToastLevel.Error);
|
|
}
|
|
}
|
|
|
|
void ResetForm()
|
|
{
|
|
product = new();
|
|
_names.Clear();
|
|
_descriptions.Clear();
|
|
_technicalDetails.Clear();
|
|
foreach (var lang in _languages.Keys)
|
|
{
|
|
_names[lang] = "";
|
|
_descriptions[lang] = "";
|
|
_technicalDetails[lang] = new();
|
|
}
|
|
SyncTranslations();
|
|
_editContext = new EditContext(product);
|
|
if (mediaComponent != null)
|
|
{
|
|
mediaComponent.Clear();
|
|
}
|
|
}
|
|
} |