feat: wire aspects panel and shipping cost in SingleItemView code-behind

Implements LoadAspectsAsync/RebuildAspectFields, real PostageBox and AiAspects
handlers, ShippingCostBox wiring, required-aspects validation, and aspects
reset on new listing. Also registers EbayAspectsService in MainWindow (Task 6).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Foster
2026-04-15 09:34:07 +01:00
parent f7b34b6a75
commit a22e11b2f7
2 changed files with 160 additions and 5 deletions

View File

@@ -12,6 +12,7 @@ public partial class MainWindow : MetroWindow
private readonly EbayListingService _listingService;
private readonly EbayCategoryService _categoryService;
private readonly AiAssistantService _aiService;
private readonly EbayAspectsService _aspectsService;
private readonly BulkImportService _bulkService;
private readonly SavedListingsService _savedService;
private readonly EbayPriceResearchService _priceService;
@@ -26,6 +27,7 @@ public partial class MainWindow : MetroWindow
_categoryService = new EbayCategoryService(_auth);
_listingService = new EbayListingService(_auth, _categoryService);
_aiService = new AiAssistantService(config);
_aspectsService = new EbayAspectsService(_auth);
_bulkService = new BulkImportService();
_savedService = new SavedListingsService();
_priceService = new EbayPriceResearchService(_auth);
@@ -39,7 +41,7 @@ public partial class MainWindow : MetroWindow
SavedView.Initialise(_savedService, _priceLookupService);
// New Listing + Bulk tabs
SingleView.Initialise(_listingService, _categoryService, _aiService, _auth);
SingleView.Initialise(_listingService, _categoryService, _aiService, _auth, _aspectsService);
BulkView.Initialise(_listingService, _categoryService, _aiService, _bulkService, _auth);
// Try to restore saved eBay session

View File

@@ -6,6 +6,7 @@ using System.Windows.Media.Imaging;
using EbayListingTool.Models;
using EbayListingTool.Services;
using Microsoft.Win32;
using System.Linq;
namespace EbayListingTool.Views;
@@ -15,6 +16,8 @@ public partial class SingleItemView : UserControl
private EbayCategoryService? _categoryService;
private AiAssistantService? _aiService;
private EbayAuthService? _auth;
private EbayAspectsService? _aspectsService;
private List<CategoryAspect> _currentAspects = new();
private ListingDraft _draft = new();
private System.Threading.CancellationTokenSource? _categoryCts;
@@ -29,6 +32,8 @@ public partial class SingleItemView : UserControl
{
InitializeComponent();
PostcodeBox.TextChanged += (s, e) => _draft.Postcode = PostcodeBox.Text;
ShippingCostBox.ValueChanged += (s, e) =>
_draft.ShippingCost = (decimal)(ShippingCostBox.Value ?? 0);
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
@@ -40,12 +45,13 @@ public partial class SingleItemView : UserControl
}
public void Initialise(EbayListingService listingService, EbayCategoryService categoryService,
AiAssistantService aiService, EbayAuthService auth)
AiAssistantService aiService, EbayAuthService auth, EbayAspectsService aspectsService)
{
_listingService = listingService;
_categoryService = categoryService;
_aiService = aiService;
_auth = auth;
_aspectsService = aspectsService;
PostcodeBox.Text = App.Configuration["Ebay:DefaultPostcode"] ?? "";
}
@@ -56,6 +62,8 @@ public partial class SingleItemView : UserControl
// Q6: reset form directly — calling NewListing_Click shows a confirmation dialog which
// is unexpected when arriving here automatically from the Photo Analysis tab.
_draft = new ListingDraft { Postcode = PostcodeBox.Text };
_currentAspects = new();
AspectsPanel.Visibility = Visibility.Collapsed;
TitleBox.Text = "";
DescriptionBox.Text = "";
CategoryBox.Text = "";
@@ -215,6 +223,7 @@ public partial class SingleItemView : UserControl
CategoryBox.Text = cat.CategoryName;
CategoryIdLabel.Text = $"ID: {cat.CategoryId}";
CategorySuggestionsList.Visibility = Visibility.Collapsed;
_ = LoadAspectsAsync(_draft.CategoryId);
}
}
@@ -239,6 +248,7 @@ public partial class SingleItemView : UserControl
_draft.CategoryName = top.CategoryName;
CategoryBox.Text = top.CategoryName;
CategoryIdLabel.Text = $"ID: {top.CategoryId}";
_ = LoadAspectsAsync(_draft.CategoryId);
}
finally { _suppressCategoryLookup = false; }
@@ -628,6 +638,8 @@ public partial class SingleItemView : UserControl
}
_draft = new ListingDraft { Postcode = PostcodeBox.Text };
_currentAspects = new();
AspectsPanel.Visibility = Visibility.Collapsed;
TitleBox.Text = "";
DescriptionBox.Text = "";
CategoryBox.Text = "";
@@ -654,6 +666,18 @@ public partial class SingleItemView : UserControl
{ ShowError("Validation", "Please select a category."); return false; }
if ((PriceBox.Value ?? 0) <= 0)
{ ShowError("Validation", "Please enter a price greater than zero."); return false; }
var missingRequired = _currentAspects
.Where(a => a.IsRequired && !_draft.Aspects.ContainsKey(a.Name))
.Select(a => a.Name)
.ToList();
if (missingRequired.Count > 0)
{
ShowError("Validation",
$"Please fill in required item specifics:\n• {string.Join("\n ", missingRequired)}");
return false;
}
return true;
}
@@ -694,12 +718,141 @@ public partial class SingleItemView : UserControl
private void PostageBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
// TODO: Task 5 — update ShippingCostBox default based on selected postage option
var idx = PostageBox.SelectedIndex;
if (ShippingCostBox != null)
ShippingCostBox.IsEnabled = idx != 4 && idx != 5; // disable for Free/Collection
_draft.Postage = idx switch
{
0 => PostageOption.RoyalMailFirstClass,
1 => PostageOption.RoyalMailSecondClass,
2 => PostageOption.RoyalMailTracked24,
3 => PostageOption.RoyalMailTracked48,
4 => PostageOption.FreePostage,
5 => PostageOption.CollectionOnly,
_ => PostageOption.RoyalMailSecondClass
};
}
private async void AiAspects_Click(object sender, RoutedEventArgs e)
{
// TODO: Task 5 — call EbayAspectsService.SuggestAspectsAsync and populate AspectsItemsControl
await Task.CompletedTask;
if (_aiService == null || _currentAspects.Count == 0) return;
AspectsAiSpinner.Visibility = Visibility.Visible;
AspectsAiIcon.Visibility = Visibility.Collapsed;
AiAspectsBtn.IsEnabled = false;
try
{
var suggestions = await _aiService.SuggestAspectsAsync(
_draft.Title, _draft.Description, _currentAspects);
foreach (var (name, value) in suggestions)
_draft.Aspects[name] = value;
RebuildAspectFields();
}
catch (Exception ex)
{
ShowError("AI Aspects", ex.Message);
}
finally
{
AspectsAiSpinner.Visibility = Visibility.Collapsed;
AspectsAiIcon.Visibility = Visibility.Visible;
AiAspectsBtn.IsEnabled = true;
}
}
private async Task LoadAspectsAsync(string categoryId)
{
if (_aspectsService == null || string.IsNullOrEmpty(categoryId)) return;
AspectsSpinner.Visibility = Visibility.Visible;
AspectsPanel.Visibility = Visibility.Visible;
AspectsItemsControl.ItemsSource = null;
try
{
_currentAspects = await _aspectsService.GetAspectsAsync(categoryId);
_draft.Aspects.Clear();
AspectsRequiredNote.Visibility = _currentAspects.Any(a => a.IsRequired)
? Visibility.Visible : Visibility.Collapsed;
RebuildAspectFields();
}
finally
{
AspectsSpinner.Visibility = Visibility.Collapsed;
}
}
private void RebuildAspectFields()
{
var panels = new List<UIElement>();
foreach (var aspect in _currentAspects)
{
var labelPanel = new StackPanel { Orientation = Orientation.Horizontal };
labelPanel.Children.Add(new TextBlock
{
Text = aspect.Name,
Style = (Style)FindResource("FieldLabel"),
Margin = new Thickness(0, 0, 0, 2)
});
if (aspect.IsRequired)
labelPanel.Children.Add(new TextBlock
{
Text = " *",
Foreground = System.Windows.Media.Brushes.OrangeRed,
FontWeight = FontWeights.Bold,
VerticalAlignment = VerticalAlignment.Bottom
});
UIElement input;
_draft.Aspects.TryGetValue(aspect.Name, out var existing);
if (!aspect.IsFreeText && aspect.AllowedValues.Count > 0)
{
var cb = new ComboBox { Width = 130, Margin = new Thickness(0, 0, 0, 8) };
cb.Items.Add(new ComboBoxItem { Content = "", Tag = "" });
foreach (var v in aspect.AllowedValues)
cb.Items.Add(new ComboBoxItem { Content = v, Tag = v });
if (!string.IsNullOrEmpty(existing))
{
var match = cb.Items.Cast<ComboBoxItem>()
.FirstOrDefault(i => i.Tag?.ToString() == existing);
if (match != null) cb.SelectedItem = match;
}
var aspectName = aspect.Name;
cb.SelectionChanged += (s, e) =>
{
var val = (cb.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "";
if (string.IsNullOrEmpty(val)) _draft.Aspects.Remove(aspectName);
else _draft.Aspects[aspectName] = val;
};
input = cb;
}
else
{
var tb = new TextBox { Width = 130, Margin = new Thickness(0, 0, 0, 8), Text = existing ?? "" };
var aspectName = aspect.Name;
tb.TextChanged += (s, e) =>
{
if (string.IsNullOrEmpty(tb.Text)) _draft.Aspects.Remove(aspectName);
else _draft.Aspects[aspectName] = tb.Text;
};
input = tb;
}
var cell = new StackPanel { Margin = new Thickness(0, 0, 12, 0) };
cell.Children.Add(labelPanel);
cell.Children.Add(input);
panels.Add(cell);
}
AspectsItemsControl.ItemsSource = panels;
AspectsPanel.Visibility = _currentAspects.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
}
}