Files
Dianaka123 697ae44519 feat: единый файл Лист1+СПРАВКА, фильтры, сортировка, живой п/п
- Загрузка одного .xlsx (СПРАВКА + Лист1 в одном файле)
- Рег. номера берутся из СПРАВКА автоматически, группируются по (ТН ВЭД, Страна)
- Отдельный опциональный справочник кодов (CodesImportService)
- Фильтры: Все / Авто / Проверить / Нет кода / Без нетто
- Таблица сразу отсортирована по ТН ВЭД с живым обновлением п/п
- Начальный п/п: поле в UI, автоинкремент после экспорта
- Лист2: рамки, жёлтая строка при >1 рег. номере, итого по кол-ву
- Лист3: ТН ВЭД в колонке B вместо константы 09035
- Красный ErrorMessage под кнопкой загрузки, удалён SpravkaFileEntry
2026-04-07 11:47:31 +03:00

301 lines
11 KiB
C#

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<DeclarationItem> _allItems = new();
// Отфильтрованный список для отображения в таблице
public ObservableCollection<DeclarationItem> 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<CodeLookupEntry>());
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));
}
}