From a22e11b2f74148852461b480d3c490fa4bf30926 Mon Sep 17 00:00:00 2001 From: Peter Foster Date: Wed, 15 Apr 2026 09:34:07 +0100 Subject: [PATCH] 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 --- EbayListingTool/Views/MainWindow.xaml.cs | 4 +- EbayListingTool/Views/SingleItemView.xaml.cs | 161 ++++++++++++++++++- 2 files changed, 160 insertions(+), 5 deletions(-) diff --git a/EbayListingTool/Views/MainWindow.xaml.cs b/EbayListingTool/Views/MainWindow.xaml.cs index d1175c3..40aa1b6 100644 --- a/EbayListingTool/Views/MainWindow.xaml.cs +++ b/EbayListingTool/Views/MainWindow.xaml.cs @@ -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 diff --git a/EbayListingTool/Views/SingleItemView.xaml.cs b/EbayListingTool/Views/SingleItemView.xaml.cs index 6cae1b3..164f089 100644 --- a/EbayListingTool/Views/SingleItemView.xaml.cs +++ b/EbayListingTool/Views/SingleItemView.xaml.cs @@ -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 _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(); + + 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() + .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; } }