using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DeclarationAutomatization.Models; using DeclarationAutomatization.Services; namespace DeclarationAutomatization.ViewModels; public partial class MainViewModel : ObservableObject { private readonly Sheet1ImportService _sheet1ImportService; private readonly ExcelImportService _spravkaImportService; private readonly CodesImportService _codesImportService; private readonly TransformService _transformService; private readonly CodeLookupService _codeLookupService; private readonly Sheet3ExpandService _expandService; private readonly ExcelExportService _exportService; // Все позиции после обработки (полный список для экспорта) private readonly List _allItems = new(); // Отфильтрованный список для отображения в таблице public ObservableCollection FilteredItems { get; } = new(); [ObservableProperty] private int _startingNumber = 1; [ObservableProperty] private string _declarationFileName = ""; [ObservableProperty] private string _codesFileName = ""; [ObservableProperty] private string _statusMessage = ""; [ObservableProperty] private string _errorMessage = ""; [ObservableProperty] private bool _isProcessing; [ObservableProperty] private bool _hasResults; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsFilterAll))] [NotifyPropertyChangedFor(nameof(IsFilterAuto))] [NotifyPropertyChangedFor(nameof(IsFilterReview))] [NotifyPropertyChangedFor(nameof(IsFilterMissing))] [NotifyPropertyChangedFor(nameof(IsFilterNoNetWeight))] private string _activeFilter = "All"; public bool IsFilterAll => ActiveFilter == "All"; public bool IsFilterAuto => ActiveFilter == "Auto"; public bool IsFilterReview => ActiveFilter == "Review"; public bool IsFilterMissing => ActiveFilter == "Missing"; public bool IsFilterNoNetWeight => ActiveFilter == "NoNetWeight"; public int AutoCount => _allItems.Count(i => i.Confidence == ConfidenceLevel.Auto); public int ReviewCount => _allItems.Count(i => i.Confidence == ConfidenceLevel.Review); public int MissingCount => _allItems.Count(i => i.Confidence == ConfidenceLevel.Missing); public int MissingNetWeightCount => _allItems.Count(i => i.NetWeight == 0); private string _filePath = ""; public MainViewModel( Sheet1ImportService sheet1ImportService, ExcelImportService spravkaImportService, CodesImportService codesImportService, TransformService transformService, CodeLookupService codeLookupService, Sheet3ExpandService expandService, ExcelExportService exportService) { _sheet1ImportService = sheet1ImportService; _spravkaImportService = spravkaImportService; _codesImportService = codesImportService; _transformService = transformService; _codeLookupService = codeLookupService; _expandService = expandService; _exportService = exportService; } [RelayCommand] private async Task OpenDeclarationFileAsync() { var dialog = new Microsoft.Win32.OpenFileDialog { Title = "Выберите файл декларации", Filter = "Excel файлы (*.xlsx)|*.xlsx", }; if (dialog.ShowDialog() != true) return; _filePath = dialog.FileName; DeclarationFileName = Path.GetFileName(_filePath); await ProcessAsync(); } [RelayCommand] private void ClearDeclarationFile() { _filePath = ""; DeclarationFileName = ""; _allItems.Clear(); FilteredItems.Clear(); HasResults = false; ActiveFilter = "All"; NotifyStats(); StatusMessage = "Загрузите файл декларации для начала работы"; } [RelayCommand] private void ClearCodesFile() { _codeLookupService.LoadFromEntries(new System.Collections.Generic.List()); CodesFileName = ""; StatusMessage = "Справочник кодов очищен"; if (_allItems.Count > 0) { _codeLookupService.AssignCodes(_allItems); RefreshFilter(); NotifyStats(); } } [RelayCommand] private void LoadCodesFile() { var dialog = new Microsoft.Win32.OpenFileDialog { Title = "Выберите справочник кодов", Filter = "Excel файлы (*.xlsx)|*.xlsx", }; if (dialog.ShowDialog() != true) return; try { var entries = _codesImportService.ReadEntries(dialog.FileName); _codeLookupService.LoadFromEntries(entries); CodesFileName = Path.GetFileName(dialog.FileName); StatusMessage = $"Справочник загружен: {entries.Count} кодов"; // Переназначить коды если декларация уже загружена if (_allItems.Count > 0) { _codeLookupService.AssignCodes(_allItems); RefreshFilter(); NotifyStats(); } } catch (Exception ex) { StatusMessage = $"Ошибка загрузки справочника: {ex.Message}"; } } [RelayCommand] private void SetFilter(string filter) { ActiveFilter = filter; RefreshFilter(); } private async Task ProcessAsync() { if (string.IsNullOrEmpty(_filePath)) return; IsProcessing = true; HasResults = false; ErrorMessage = ""; StatusMessage = "Обработка..."; try { await Task.Run(() => { var groups = _sheet1ImportService.ReadSheet1(_filePath); var spravkaItems = _spravkaImportService.ReadSpravka(_filePath, 1); var items = _transformService.BuildDeclarationItems(groups, spravkaItems); _codeLookupService.AssignCodes(items); Application.Current.Dispatcher.Invoke(() => { _allItems.Clear(); _allItems.AddRange(items.OrderBy(i => i.TnVed)); RenumberItems(); RefreshFilter(); NotifyStats(); HasResults = _allItems.Count > 0; StatusMessage = $"Загружено {_allItems.Count} позиций: " + $"✅ {AutoCount} ⚠️ {ReviewCount} 🔴 {MissingCount}"; }); }); } catch (Exception ex) { _filePath = ""; DeclarationFileName = ""; ErrorMessage = FriendlyError(ex); StatusMessage = ""; } finally { IsProcessing = false; } } [RelayCommand] private async Task ExportAsync() { if (!HasResults) { StatusMessage = "Нет данных для экспорта. Сначала загрузите файл."; return; } var dialog = new Microsoft.Win32.SaveFileDialog { Title = "Сохранить результат", Filter = "Excel файлы (*.xlsx)|*.xlsx", FileName = $"Декларация_{DateTime.Now:yyyyMMdd_HHmm}.xlsx", }; if (dialog.ShowDialog() != true) return; IsProcessing = true; StatusMessage = "Экспорт..."; try { // _allItems уже отсортированы и перенумерованы — просто экспортируем var sheet3Rows = await Task.Run(() => _expandService.Expand(_allItems)); await Task.Run(() => _exportService.Export(dialog.FileName, _allItems, sheet3Rows)); // Сдвигаем стартовый номер для следующей справки StartingNumber += _allItems.Count; StatusMessage = $"Файл сохранён: {Path.GetFileName(dialog.FileName)} " + $"(Лист2: {_allItems.Count} строк, Лист3: {sheet3Rows.Count} строк)"; } catch (Exception ex) { StatusMessage = $"Ошибка экспорта: {ex.Message}"; } finally { IsProcessing = false; } } // Вызывается из UI при ручном изменении кода в таблице public void OnCodeManuallyChanged(DeclarationItem item, string newCode) { if (string.IsNullOrWhiteSpace(item.TnVed) || string.IsNullOrWhiteSpace(newCode)) return; item.DeclarationCode = newCode; item.Confidence = ConfidenceLevel.Auto; _codeLookupService.LearnFromManualEdit(item.TnVed, newCode); NotifyStats(); } private void RefreshFilter() { FilteredItems.Clear(); var items = ActiveFilter switch { "Auto" => _allItems.Where(i => i.Confidence == ConfidenceLevel.Auto), "Review" => _allItems.Where(i => i.Confidence == ConfidenceLevel.Review), "Missing" => _allItems.Where(i => i.Confidence == ConfidenceLevel.Missing), "NoNetWeight" => _allItems.Where(i => i.NetWeight == 0), _ => _allItems.AsEnumerable(), }; foreach (var item in items) FilteredItems.Add(item); } partial void OnStartingNumberChanged(int value) { if (_allItems.Count == 0) return; RenumberItems(); RefreshFilter(); } private void RenumberItems() { for (int i = 0; i < _allItems.Count; i++) _allItems[i].SequentialNumber = StartingNumber + i; } private static string FriendlyError(Exception ex) { if (ex is IOException || ex.InnerException is IOException) return "Файл открыт в другой программе. Закройте его и попробуйте снова."; return ex.Message; } private void NotifyStats() { OnPropertyChanged(nameof(AutoCount)); OnPropertyChanged(nameof(ReviewCount)); OnPropertyChanged(nameof(MissingCount)); OnPropertyChanged(nameof(MissingNetWeightCount)); } }