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:
@@ -12,6 +12,7 @@ public partial class MainWindow : MetroWindow
|
|||||||
private readonly EbayListingService _listingService;
|
private readonly EbayListingService _listingService;
|
||||||
private readonly EbayCategoryService _categoryService;
|
private readonly EbayCategoryService _categoryService;
|
||||||
private readonly AiAssistantService _aiService;
|
private readonly AiAssistantService _aiService;
|
||||||
|
private readonly EbayAspectsService _aspectsService;
|
||||||
private readonly BulkImportService _bulkService;
|
private readonly BulkImportService _bulkService;
|
||||||
private readonly SavedListingsService _savedService;
|
private readonly SavedListingsService _savedService;
|
||||||
private readonly EbayPriceResearchService _priceService;
|
private readonly EbayPriceResearchService _priceService;
|
||||||
@@ -26,6 +27,7 @@ public partial class MainWindow : MetroWindow
|
|||||||
_categoryService = new EbayCategoryService(_auth);
|
_categoryService = new EbayCategoryService(_auth);
|
||||||
_listingService = new EbayListingService(_auth, _categoryService);
|
_listingService = new EbayListingService(_auth, _categoryService);
|
||||||
_aiService = new AiAssistantService(config);
|
_aiService = new AiAssistantService(config);
|
||||||
|
_aspectsService = new EbayAspectsService(_auth);
|
||||||
_bulkService = new BulkImportService();
|
_bulkService = new BulkImportService();
|
||||||
_savedService = new SavedListingsService();
|
_savedService = new SavedListingsService();
|
||||||
_priceService = new EbayPriceResearchService(_auth);
|
_priceService = new EbayPriceResearchService(_auth);
|
||||||
@@ -39,7 +41,7 @@ public partial class MainWindow : MetroWindow
|
|||||||
SavedView.Initialise(_savedService, _priceLookupService);
|
SavedView.Initialise(_savedService, _priceLookupService);
|
||||||
|
|
||||||
// New Listing + Bulk tabs
|
// New Listing + Bulk tabs
|
||||||
SingleView.Initialise(_listingService, _categoryService, _aiService, _auth);
|
SingleView.Initialise(_listingService, _categoryService, _aiService, _auth, _aspectsService);
|
||||||
BulkView.Initialise(_listingService, _categoryService, _aiService, _bulkService, _auth);
|
BulkView.Initialise(_listingService, _categoryService, _aiService, _bulkService, _auth);
|
||||||
|
|
||||||
// Try to restore saved eBay session
|
// Try to restore saved eBay session
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Windows.Media.Imaging;
|
|||||||
using EbayListingTool.Models;
|
using EbayListingTool.Models;
|
||||||
using EbayListingTool.Services;
|
using EbayListingTool.Services;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace EbayListingTool.Views;
|
namespace EbayListingTool.Views;
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ public partial class SingleItemView : UserControl
|
|||||||
private EbayCategoryService? _categoryService;
|
private EbayCategoryService? _categoryService;
|
||||||
private AiAssistantService? _aiService;
|
private AiAssistantService? _aiService;
|
||||||
private EbayAuthService? _auth;
|
private EbayAuthService? _auth;
|
||||||
|
private EbayAspectsService? _aspectsService;
|
||||||
|
private List<CategoryAspect> _currentAspects = new();
|
||||||
|
|
||||||
private ListingDraft _draft = new();
|
private ListingDraft _draft = new();
|
||||||
private System.Threading.CancellationTokenSource? _categoryCts;
|
private System.Threading.CancellationTokenSource? _categoryCts;
|
||||||
@@ -29,6 +32,8 @@ public partial class SingleItemView : UserControl
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
PostcodeBox.TextChanged += (s, e) => _draft.Postcode = PostcodeBox.Text;
|
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)
|
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
||||||
@@ -40,12 +45,13 @@ public partial class SingleItemView : UserControl
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void Initialise(EbayListingService listingService, EbayCategoryService categoryService,
|
public void Initialise(EbayListingService listingService, EbayCategoryService categoryService,
|
||||||
AiAssistantService aiService, EbayAuthService auth)
|
AiAssistantService aiService, EbayAuthService auth, EbayAspectsService aspectsService)
|
||||||
{
|
{
|
||||||
_listingService = listingService;
|
_listingService = listingService;
|
||||||
_categoryService = categoryService;
|
_categoryService = categoryService;
|
||||||
_aiService = aiService;
|
_aiService = aiService;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
|
_aspectsService = aspectsService;
|
||||||
|
|
||||||
PostcodeBox.Text = App.Configuration["Ebay:DefaultPostcode"] ?? "";
|
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
|
// Q6: reset form directly — calling NewListing_Click shows a confirmation dialog which
|
||||||
// is unexpected when arriving here automatically from the Photo Analysis tab.
|
// is unexpected when arriving here automatically from the Photo Analysis tab.
|
||||||
_draft = new ListingDraft { Postcode = PostcodeBox.Text };
|
_draft = new ListingDraft { Postcode = PostcodeBox.Text };
|
||||||
|
_currentAspects = new();
|
||||||
|
AspectsPanel.Visibility = Visibility.Collapsed;
|
||||||
TitleBox.Text = "";
|
TitleBox.Text = "";
|
||||||
DescriptionBox.Text = "";
|
DescriptionBox.Text = "";
|
||||||
CategoryBox.Text = "";
|
CategoryBox.Text = "";
|
||||||
@@ -215,6 +223,7 @@ public partial class SingleItemView : UserControl
|
|||||||
CategoryBox.Text = cat.CategoryName;
|
CategoryBox.Text = cat.CategoryName;
|
||||||
CategoryIdLabel.Text = $"ID: {cat.CategoryId}";
|
CategoryIdLabel.Text = $"ID: {cat.CategoryId}";
|
||||||
CategorySuggestionsList.Visibility = Visibility.Collapsed;
|
CategorySuggestionsList.Visibility = Visibility.Collapsed;
|
||||||
|
_ = LoadAspectsAsync(_draft.CategoryId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +248,7 @@ public partial class SingleItemView : UserControl
|
|||||||
_draft.CategoryName = top.CategoryName;
|
_draft.CategoryName = top.CategoryName;
|
||||||
CategoryBox.Text = top.CategoryName;
|
CategoryBox.Text = top.CategoryName;
|
||||||
CategoryIdLabel.Text = $"ID: {top.CategoryId}";
|
CategoryIdLabel.Text = $"ID: {top.CategoryId}";
|
||||||
|
_ = LoadAspectsAsync(_draft.CategoryId);
|
||||||
}
|
}
|
||||||
finally { _suppressCategoryLookup = false; }
|
finally { _suppressCategoryLookup = false; }
|
||||||
|
|
||||||
@@ -628,6 +638,8 @@ public partial class SingleItemView : UserControl
|
|||||||
}
|
}
|
||||||
|
|
||||||
_draft = new ListingDraft { Postcode = PostcodeBox.Text };
|
_draft = new ListingDraft { Postcode = PostcodeBox.Text };
|
||||||
|
_currentAspects = new();
|
||||||
|
AspectsPanel.Visibility = Visibility.Collapsed;
|
||||||
TitleBox.Text = "";
|
TitleBox.Text = "";
|
||||||
DescriptionBox.Text = "";
|
DescriptionBox.Text = "";
|
||||||
CategoryBox.Text = "";
|
CategoryBox.Text = "";
|
||||||
@@ -654,6 +666,18 @@ public partial class SingleItemView : UserControl
|
|||||||
{ ShowError("Validation", "Please select a category."); return false; }
|
{ ShowError("Validation", "Please select a category."); return false; }
|
||||||
if ((PriceBox.Value ?? 0) <= 0)
|
if ((PriceBox.Value ?? 0) <= 0)
|
||||||
{ ShowError("Validation", "Please enter a price greater than zero."); return false; }
|
{ 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,12 +718,141 @@ public partial class SingleItemView : UserControl
|
|||||||
|
|
||||||
private void PostageBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
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)
|
private async void AiAspects_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// TODO: Task 5 — call EbayAspectsService.SuggestAspectsAsync and populate AspectsItemsControl
|
if (_aiService == null || _currentAspects.Count == 0) return;
|
||||||
await Task.CompletedTask;
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user