diff --git a/XLAB.sln b/XLAB.sln
index 50ba80c..9379f38 100644
--- a/XLAB.sln
+++ b/XLAB.sln
@@ -5,20 +5,54 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XLAB.DATA", "XLAB.DATA\XLAB
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XLAB", "XLAB\XLAB.csproj", "{B8DAAB84-777A-4274-8452-E602DB1AF587}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XLAB2", "XLAB2\XLAB2.csproj", "{6B248955-05FF-43E9-B038-5CD501D21442}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AE0E35D7-DFA4-4150-9889-255043B232BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE0E35D7-DFA4-4150-9889-255043B232BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Debug|x64.Build.0 = Debug|Any CPU
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Debug|x86.Build.0 = Debug|Any CPU
{AE0E35D7-DFA4-4150-9889-255043B232BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE0E35D7-DFA4-4150-9889-255043B232BB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Release|x64.ActiveCfg = Release|Any CPU
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Release|x64.Build.0 = Release|Any CPU
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Release|x86.ActiveCfg = Release|Any CPU
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Release|x86.Build.0 = Release|Any CPU
{B8DAAB84-777A-4274-8452-E602DB1AF587}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8DAAB84-777A-4274-8452-E602DB1AF587}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Debug|x64.Build.0 = Debug|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Debug|x86.Build.0 = Debug|Any CPU
{B8DAAB84-777A-4274-8452-E602DB1AF587}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8DAAB84-777A-4274-8452-E602DB1AF587}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Release|x64.ActiveCfg = Release|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Release|x64.Build.0 = Release|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Release|x86.ActiveCfg = Release|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Release|x86.Build.0 = Release|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Debug|x64.Build.0 = Debug|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Debug|x86.Build.0 = Debug|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Release|x64.ActiveCfg = Release|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Release|x64.Build.0 = Release|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Release|x86.ActiveCfg = Release|Any CPU
+ {6B248955-05FF-43E9-B038-5CD501D21442}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/XLAB.slnx b/XLAB.slnx
index 5b5d087..6da51ee 100644
--- a/XLAB.slnx
+++ b/XLAB.slnx
@@ -1,4 +1,5 @@
+
diff --git a/XLAB2/App.xaml b/XLAB2/App.xaml
new file mode 100644
index 0000000..f66582e
--- /dev/null
+++ b/XLAB2/App.xaml
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XLAB2/App.xaml.cs b/XLAB2/App.xaml.cs
new file mode 100644
index 0000000..4da3e08
--- /dev/null
+++ b/XLAB2/App.xaml.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Markup;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using XLAB2.Infrastructure;
+
+namespace XLAB2
+{
+ public partial class App : Application
+ {
+ private IHost _host;
+
+ public App()
+ {
+ ApplyRussianCulture();
+ }
+
+ protected override async void OnStartup(StartupEventArgs e)
+ {
+ base.OnStartup(e);
+
+ try
+ {
+ _host = AppHost.Create();
+ await _host.StartAsync().ConfigureAwait(true);
+
+ MainWindow = _host.Services.GetRequiredService();
+ MainWindow.Show();
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(ex.Message, "XLAB2", MessageBoxButton.OK, MessageBoxImage.Error);
+ Shutdown(-1);
+ }
+ }
+
+ protected override async void OnExit(ExitEventArgs e)
+ {
+ if (_host != null)
+ {
+ try
+ {
+ await _host.StopAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(true);
+ }
+ finally
+ {
+ _host.Dispose();
+ }
+ }
+
+ base.OnExit(e);
+ }
+
+ private static void ApplyRussianCulture()
+ {
+ var culture = new CultureInfo("ru-RU");
+
+ CultureInfo.DefaultThreadCurrentCulture = culture;
+ CultureInfo.DefaultThreadCurrentUICulture = culture;
+ Thread.CurrentThread.CurrentCulture = culture;
+ Thread.CurrentThread.CurrentUICulture = culture;
+
+ FrameworkElement.LanguageProperty.OverrideMetadata(
+ typeof(FrameworkElement),
+ new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(culture.IetfLanguageTag)));
+ }
+ }
+}
diff --git a/XLAB2/AppHost.cs b/XLAB2/AppHost.cs
new file mode 100644
index 0000000..c1d748f
--- /dev/null
+++ b/XLAB2/AppHost.cs
@@ -0,0 +1,36 @@
+using System;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using XLAB2.Infrastructure;
+
+namespace XLAB2
+{
+ internal static class AppHost
+ {
+ public static IHost Create()
+ {
+ return Host.CreateDefaultBuilder()
+ .UseContentRoot(AppContext.BaseDirectory)
+ .ConfigureAppConfiguration((context, config) =>
+ {
+ config.Sources.Clear();
+ config.SetBasePath(AppContext.BaseDirectory);
+ config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
+ config.AddEnvironmentVariables();
+ })
+ .ConfigureServices((_, services) =>
+ {
+ services.AddSingleton(_ => SqlServerConnectionFactory.Current);
+ services.AddTransient();
+ services.AddTransient(provider => new MainWindow(provider.GetRequiredService()));
+ })
+ .UseDefaultServiceProvider((_, options) =>
+ {
+ options.ValidateOnBuild = true;
+ options.ValidateScopes = true;
+ })
+ .Build();
+ }
+ }
+}
diff --git a/XLAB2/AssemblyInfo.cs b/XLAB2/AssemblyInfo.cs
new file mode 100644
index 0000000..cc29e7f
--- /dev/null
+++ b/XLAB2/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly:ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/XLAB2/CloneVerificationWindow.xaml b/XLAB2/CloneVerificationWindow.xaml
new file mode 100644
index 0000000..dec575d
--- /dev/null
+++ b/XLAB2/CloneVerificationWindow.xaml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XLAB2/CloneVerificationWindow.xaml.cs b/XLAB2/CloneVerificationWindow.xaml.cs
new file mode 100644
index 0000000..2d8de6f
--- /dev/null
+++ b/XLAB2/CloneVerificationWindow.xaml.cs
@@ -0,0 +1,20 @@
+using System.Windows;
+
+namespace XLAB2
+{
+ public partial class CloneVerificationWindow : Window
+ {
+ internal CloneVerificationWindow(CloneVerificationWindowViewModel viewModel)
+ {
+ InitializeComponent();
+ DataContext = viewModel;
+ viewModel.CloseRequested += ViewModelOnCloseRequested;
+ }
+
+ private void ViewModelOnCloseRequested(object sender, bool? dialogResult)
+ {
+ DialogResult = dialogResult;
+ Close();
+ }
+ }
+}
diff --git a/XLAB2/CloneVerificationWindowViewModel.cs b/XLAB2/CloneVerificationWindowViewModel.cs
new file mode 100644
index 0000000..4265f46
--- /dev/null
+++ b/XLAB2/CloneVerificationWindowViewModel.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using System.Windows.Input;
+
+namespace XLAB2
+{
+ internal sealed class CloneVerificationWindowViewModel : ObservableObject
+ {
+ private string _serialNumbersText;
+ private string _statusText;
+
+ public CloneVerificationWindowViewModel(CloneVerificationSeed seed)
+ {
+ if (seed == null)
+ {
+ throw new ArgumentNullException("seed");
+ }
+
+ SourceSerialNumber = string.IsNullOrWhiteSpace(seed.SourceSerialNumber) ? string.Empty : seed.SourceSerialNumber.Trim();
+ VerificationSummary = seed.VerificationSummary ?? string.Empty;
+
+ ConfirmCommand = new RelayCommand(Confirm, CanConfirm);
+ CancelCommand = new RelayCommand(Cancel);
+ UpdateStatus();
+ }
+
+ public event EventHandler CloseRequested;
+
+ public ICommand CancelCommand { get; private set; }
+
+ public ICommand ConfirmCommand { get; private set; }
+
+ public string SourceSerialNumber { get; private set; }
+
+ public string SerialNumbersText
+ {
+ get { return _serialNumbersText; }
+ set
+ {
+ if (SetProperty(ref _serialNumbersText, value))
+ {
+ UpdateStatus();
+ ((RelayCommand)ConfirmCommand).RaiseCanExecuteChanged();
+ }
+ }
+ }
+
+ public string StatusText
+ {
+ get { return _statusText; }
+ private set { SetProperty(ref _statusText, value); }
+ }
+
+ public string VerificationSummary { get; private set; }
+
+ public IReadOnlyList GetSerialNumbers()
+ {
+ return ParseSerialNumbers(SerialNumbersText);
+ }
+
+ private void Cancel(object parameter)
+ {
+ RaiseCloseRequested(false);
+ }
+
+ private bool CanConfirm(object parameter)
+ {
+ return ParseSerialNumbers(SerialNumbersText).Count > 0;
+ }
+
+ private void Confirm(object parameter)
+ {
+ RaiseCloseRequested(true);
+ }
+
+ private static List ParseSerialNumbers(string value)
+ {
+ var serialNumbers = new List();
+ var unique = new HashSet(StringComparer.OrdinalIgnoreCase);
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return serialNumbers;
+ }
+
+ var separators = new[] { '\r', '\n', '\t', ',', ';' };
+ foreach (var token in value.Split(separators, StringSplitOptions.RemoveEmptyEntries))
+ {
+ var serialNumber = token.Trim();
+ if (serialNumber.Length == 0 || !unique.Add(serialNumber))
+ {
+ continue;
+ }
+
+ serialNumbers.Add(serialNumber);
+ }
+
+ return serialNumbers;
+ }
+
+ private void RaiseCloseRequested(bool? dialogResult)
+ {
+ var handler = CloseRequested;
+ if (handler != null)
+ {
+ handler(this, dialogResult);
+ }
+ }
+
+ private void UpdateStatus()
+ {
+ var serialCount = ParseSerialNumbers(SerialNumbersText).Count;
+ StatusText = string.Format("Уникальных заводских номеров: {0}.", serialCount);
+ }
+ }
+}
diff --git a/XLAB2/CreateDocumentWindow.xaml b/XLAB2/CreateDocumentWindow.xaml
new file mode 100644
index 0000000..bcb0aad
--- /dev/null
+++ b/XLAB2/CreateDocumentWindow.xaml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XLAB2/CreateDocumentWindow.xaml.cs b/XLAB2/CreateDocumentWindow.xaml.cs
new file mode 100644
index 0000000..27cbc7b
--- /dev/null
+++ b/XLAB2/CreateDocumentWindow.xaml.cs
@@ -0,0 +1,20 @@
+using System.Windows;
+
+namespace XLAB2
+{
+ public partial class CreateDocumentWindow : Window
+ {
+ internal CreateDocumentWindow(CreateDocumentWindowViewModel viewModel)
+ {
+ InitializeComponent();
+ DataContext = viewModel;
+ viewModel.CloseRequested += ViewModelOnCloseRequested;
+ }
+
+ private void ViewModelOnCloseRequested(object sender, bool? dialogResult)
+ {
+ DialogResult = dialogResult;
+ Close();
+ }
+ }
+}
diff --git a/XLAB2/CreateDocumentWindowViewModel.cs b/XLAB2/CreateDocumentWindowViewModel.cs
new file mode 100644
index 0000000..5b6bffc
--- /dev/null
+++ b/XLAB2/CreateDocumentWindowViewModel.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Windows.Input;
+
+namespace XLAB2
+{
+ internal sealed class CreateDocumentWindowViewModel : ObservableObject
+ {
+ private DateTime? _acceptedOn;
+ private string _documentNumber;
+ private string _validationMessage;
+
+ public CreateDocumentWindowViewModel(DocumentEditorResult seed)
+ {
+ _acceptedOn = seed != null ? seed.AcceptedOn : DateTime.Today;
+ _documentNumber = seed != null ? seed.DocumentNumber : string.Empty;
+
+ ConfirmCommand = new RelayCommand(Confirm);
+ CancelCommand = new RelayCommand(Cancel);
+ }
+
+ public event EventHandler CloseRequested;
+
+ public DateTime? AcceptedOn
+ {
+ get { return _acceptedOn; }
+ set { SetProperty(ref _acceptedOn, value); }
+ }
+
+ public ICommand CancelCommand { get; private set; }
+
+ public ICommand ConfirmCommand { get; private set; }
+
+ public string DocumentNumber
+ {
+ get { return _documentNumber; }
+ set { SetProperty(ref _documentNumber, value); }
+ }
+
+ public string ValidationMessage
+ {
+ get { return _validationMessage; }
+ private set { SetProperty(ref _validationMessage, value); }
+ }
+
+ public DocumentEditorResult ToResult()
+ {
+ return new DocumentEditorResult
+ {
+ DocumentNumber = DocumentNumber == null ? string.Empty : DocumentNumber.Trim(),
+ AcceptedOn = AcceptedOn.HasValue ? AcceptedOn.Value : DateTime.Today,
+ IssuedOn = null
+ };
+ }
+
+ private void Cancel(object parameter)
+ {
+ RaiseCloseRequested(false);
+ }
+
+ private void Confirm(object parameter)
+ {
+ if (string.IsNullOrWhiteSpace(DocumentNumber))
+ {
+ ValidationMessage = "Введите номер ПСВ.";
+ return;
+ }
+
+ if (!AcceptedOn.HasValue)
+ {
+ ValidationMessage = "Укажите дату приемки.";
+ return;
+ }
+
+ ValidationMessage = string.Empty;
+ RaiseCloseRequested(true);
+ }
+
+ private void RaiseCloseRequested(bool? dialogResult)
+ {
+ var handler = CloseRequested;
+ if (handler != null)
+ {
+ handler(this, dialogResult);
+ }
+ }
+ }
+}
diff --git a/XLAB2/DialogService.cs b/XLAB2/DialogService.cs
new file mode 100644
index 0000000..911ebf1
--- /dev/null
+++ b/XLAB2/DialogService.cs
@@ -0,0 +1,161 @@
+using System.Collections.Generic;
+using System.Windows;
+
+namespace XLAB2
+{
+ internal interface IDialogService
+ {
+ DocumentEditorResult ShowCreateDocumentDialog(DocumentEditorResult seed);
+
+ IReadOnlyList ShowInstrumentPickerDialog(string customerName, IReadOnlyList instruments);
+
+ InstrumentTypeSelectionResult ShowInstrumentTypeDialog(string customerName, IReadOnlyList instrumentTypes);
+
+ IReadOnlyList ShowCloneVerificationDialog(CloneVerificationSeed seed);
+
+ SpoiDirectoryItem ShowSpoiEditDialog(SpoiDirectoryItem seed, bool isNew, IReadOnlyList existingItems);
+
+ SpnmtpDirectoryItem ShowSpnmtpEditDialog(SpnmtpDirectoryItem seed, bool isNew, IReadOnlyList existingItems);
+
+ VerificationEditResult ShowVerificationDialog(
+ VerificationEditSeed seed,
+ IReadOnlyList verifiers,
+ IReadOnlyList documentForms);
+
+ bool Confirm(string message);
+
+ void ShowError(string message);
+
+ void ShowInfo(string message);
+
+ void ShowWarning(string message);
+ }
+
+ internal sealed class DialogService : IDialogService
+ {
+ public DialogService()
+ {
+ }
+
+ public DialogService(Window owner)
+ {
+ Owner = owner;
+ }
+
+ public Window Owner { get; set; }
+
+ public DocumentEditorResult ShowCreateDocumentDialog(DocumentEditorResult seed)
+ {
+ var viewModel = new CreateDocumentWindowViewModel(seed);
+ var window = new CreateDocumentWindow(viewModel);
+ AttachOwner(window);
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.ToResult() : null;
+ }
+
+ public IReadOnlyList ShowInstrumentPickerDialog(string customerName, IReadOnlyList instruments)
+ {
+ var viewModel = new SelectInstrumentsWindowViewModel(customerName, instruments);
+ var window = new SelectInstrumentsWindow(viewModel);
+ AttachOwner(window);
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.GetSelectedItems() : null;
+ }
+
+ public InstrumentTypeSelectionResult ShowInstrumentTypeDialog(string customerName, IReadOnlyList instrumentTypes)
+ {
+ var viewModel = new SelectInstrumentTypeWindowViewModel(customerName, instrumentTypes);
+ var window = new SelectInstrumentTypeWindow(viewModel);
+ AttachOwner(window);
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.GetResult() : null;
+ }
+
+ public IReadOnlyList ShowCloneVerificationDialog(CloneVerificationSeed seed)
+ {
+ var viewModel = new CloneVerificationWindowViewModel(seed);
+ var window = new CloneVerificationWindow(viewModel);
+ AttachOwner(window);
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.GetSerialNumbers() : null;
+ }
+
+ public SpoiDirectoryItem ShowSpoiEditDialog(SpoiDirectoryItem seed, bool isNew, IReadOnlyList existingItems)
+ {
+ var viewModel = new SpoiEditWindowViewModel(seed, isNew, existingItems);
+ var window = new SpoiEditWindow(viewModel);
+ AttachOwner(window);
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.ToResult() : null;
+ }
+
+ public SpnmtpDirectoryItem ShowSpnmtpEditDialog(SpnmtpDirectoryItem seed, bool isNew, IReadOnlyList existingItems)
+ {
+ var viewModel = new SpnmtpEditWindowViewModel(seed, isNew, existingItems);
+ var window = new SpnmtpEditWindow(viewModel);
+ AttachOwner(window);
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.ToResult() : null;
+ }
+
+ public VerificationEditResult ShowVerificationDialog(
+ VerificationEditSeed seed,
+ IReadOnlyList verifiers,
+ IReadOnlyList documentForms)
+ {
+ var viewModel = new VerificationEditWindowViewModel(seed, verifiers, documentForms);
+ var window = new VerificationEditWindow(viewModel);
+ AttachOwner(window);
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.ToResult() : null;
+ }
+
+ public bool Confirm(string message)
+ {
+ return Owner == null
+ ? MessageBox.Show(message, "ПСВ", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes
+ : MessageBox.Show(Owner, message, "ПСВ", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
+ }
+
+ public void ShowError(string message)
+ {
+ ShowMessage(message, MessageBoxImage.Error);
+ }
+
+ public void ShowInfo(string message)
+ {
+ ShowMessage(message, MessageBoxImage.Information);
+ }
+
+ public void ShowWarning(string message)
+ {
+ ShowMessage(message, MessageBoxImage.Warning);
+ }
+
+ private void AttachOwner(Window window)
+ {
+ if (Owner != null)
+ {
+ window.Owner = Owner;
+ }
+ }
+
+ private void ShowMessage(string message, MessageBoxImage image)
+ {
+ if (Owner == null)
+ {
+ MessageBox.Show(message, "ПСВ", MessageBoxButton.OK, image);
+ return;
+ }
+
+ MessageBox.Show(Owner, message, "ПСВ", MessageBoxButton.OK, image);
+ }
+ }
+}
diff --git a/XLAB2/FrpdDirectoryDialogService.cs b/XLAB2/FrpdDirectoryDialogService.cs
new file mode 100644
index 0000000..6fc834f
--- /dev/null
+++ b/XLAB2/FrpdDirectoryDialogService.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using System.Windows;
+
+namespace XLAB2
+{
+ internal interface IFrpdDirectoryDialogService
+ {
+ bool Confirm(string message);
+
+ FrpdDirectoryItem ShowFrpdEditDialog(FrpdDirectoryItem seed, bool isNew, IReadOnlyList existingItems, FrpdDirectoryService service);
+
+ FrpdvdDirectoryItem ShowFrpdvdEditDialog(FrpdvdDirectoryItem seed, bool isNew, IReadOnlyList existingItems, FrpdDirectoryService service);
+
+ void ShowError(string message);
+
+ void ShowInfo(string message);
+
+ void ShowWarning(string message);
+ }
+
+ internal sealed class FrpdDirectoryDialogService : IFrpdDirectoryDialogService
+ {
+ private readonly Window _owner;
+
+ public FrpdDirectoryDialogService(Window owner)
+ {
+ _owner = owner;
+ }
+
+ public bool Confirm(string message)
+ {
+ return MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
+ }
+
+ public FrpdDirectoryItem ShowFrpdEditDialog(FrpdDirectoryItem seed, bool isNew, IReadOnlyList existingItems, FrpdDirectoryService service)
+ {
+ var viewModel = new FrpdEditWindowViewModel(seed, isNew, existingItems, service);
+ var window = new FrpdEditWindow(viewModel);
+ window.Owner = _owner;
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.ToResult() : null;
+ }
+
+ public FrpdvdDirectoryItem ShowFrpdvdEditDialog(FrpdvdDirectoryItem seed, bool isNew, IReadOnlyList existingItems, FrpdDirectoryService service)
+ {
+ var viewModel = new FrpdvdEditWindowViewModel(seed, isNew, existingItems, service);
+ var window = new FrpdvdEditWindow(viewModel);
+ window.Owner = _owner;
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.ToResult() : null;
+ }
+
+ public void ShowError(string message)
+ {
+ MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+
+ public void ShowInfo(string message)
+ {
+ MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+
+ public void ShowWarning(string message)
+ {
+ MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ }
+}
diff --git a/XLAB2/FrpdDirectoryModels.cs b/XLAB2/FrpdDirectoryModels.cs
new file mode 100644
index 0000000..137f4d6
--- /dev/null
+++ b/XLAB2/FrpdDirectoryModels.cs
@@ -0,0 +1,45 @@
+using System;
+
+namespace XLAB2
+{
+ public sealed class FrpdDirectoryItem
+ {
+ public string ActivityNames { get; set; }
+
+ public DateTime? CreatedOn { get; set; }
+
+ public string Guid { get; set; }
+
+ public int Id { get; set; }
+
+ public DateTime? LiquidatedOn { get; set; }
+
+ public string LocalCode { get; set; }
+
+ public string Name { get; set; }
+
+ public int? ParentId { get; set; }
+
+ public string ParentName { get; set; }
+ }
+
+ public sealed class FrpdvdDirectoryItem
+ {
+ public string ActivityName { get; set; }
+
+ public int ActivityId { get; set; }
+
+ public int FrpdId { get; set; }
+
+ public int Id { get; set; }
+ }
+
+ internal static class FrpdDirectoryRules
+ {
+ public const int GuidMaxLength = 50;
+
+ public const int LocalCodeMaxLength = 20;
+
+ public const int NameMaxLength = 80;
+ }
+}
diff --git a/XLAB2/FrpdDirectoryService.cs b/XLAB2/FrpdDirectoryService.cs
new file mode 100644
index 0000000..a9e4fc2
--- /dev/null
+++ b/XLAB2/FrpdDirectoryService.cs
@@ -0,0 +1,560 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using Microsoft.Data.SqlClient;
+using System.Linq;
+
+namespace XLAB2
+{
+ internal sealed class FrpdDirectoryService
+ {
+ public int AddFrpdItem(FrpdDirectoryItem item)
+ {
+ var normalizedItem = NormalizeFrpdItem(item);
+ var guidForInsert = string.IsNullOrWhiteSpace(normalizedItem.Guid)
+ ? Guid.NewGuid().ToString().ToUpperInvariant()
+ : normalizedItem.Guid;
+
+ const string sql = @"
+INSERT INTO dbo.FRPD
+(
+ IDFRPDR,
+ NMFRPD,
+ KDFRPDLC,
+ FRPDGUID,
+ DTSZFRPD,
+ DTLKFRPD
+)
+VALUES
+(
+ @ParentId,
+ @Name,
+ @LocalCode,
+ @Guid,
+ @CreatedOn,
+ @LiquidatedOn
+);
+
+SELECT CAST(SCOPE_IDENTITY() AS int);";
+
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ using (var command = new SqlCommand(sql, connection))
+ {
+ connection.Open();
+ EnsureFrpdGuidIsUnique(connection, guidForInsert, null);
+
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ParentId", normalizedItem.ParentId);
+ command.Parameters.Add("@Name", SqlDbType.VarChar, FrpdDirectoryRules.NameMaxLength).Value = normalizedItem.Name;
+ ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@LocalCode", SqlDbType.VarChar, FrpdDirectoryRules.LocalCodeMaxLength, normalizedItem.LocalCode);
+ ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@Guid", SqlDbType.VarChar, FrpdDirectoryRules.GuidMaxLength, guidForInsert);
+ ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@CreatedOn", normalizedItem.CreatedOn);
+ ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@LiquidatedOn", normalizedItem.LiquidatedOn);
+
+ try
+ {
+ return Convert.ToInt32(command.ExecuteScalar());
+ }
+ catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "IX_FRPD_FRPDGUID"))
+ {
+ throw CreateFrpdDuplicateGuidException(guidForInsert);
+ }
+ }
+ }
+
+ public int AddFrpdvdItem(FrpdvdDirectoryItem item)
+ {
+ var normalizedItem = NormalizeFrpdvdItem(item);
+
+ const string sql = @"
+INSERT INTO dbo.FRPDVD
+(
+ IDFRPD,
+ IDSPVDDO
+)
+VALUES
+(
+ @FrpdId,
+ @ActivityId
+);
+
+SELECT CAST(SCOPE_IDENTITY() AS int);";
+
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ using (var command = new SqlCommand(sql, connection))
+ {
+ connection.Open();
+ EnsureFrpdvdIsUnique(connection, normalizedItem.FrpdId, normalizedItem.ActivityId, null);
+
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@FrpdId", SqlDbType.Int).Value = normalizedItem.FrpdId;
+ command.Parameters.Add("@ActivityId", SqlDbType.Int).Value = normalizedItem.ActivityId;
+
+ try
+ {
+ return Convert.ToInt32(command.ExecuteScalar());
+ }
+ catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "XAK1FRPDVD"))
+ {
+ throw CreateFrpdvdDuplicateException();
+ }
+ }
+ }
+
+ public DirectoryDeleteResult DeleteFrpdItem(int id)
+ {
+ if (id <= 0)
+ {
+ throw new InvalidOperationException("Не выбрана запись FRPD для удаления.");
+ }
+
+ const string sql = @"
+DELETE FROM dbo.FRPD
+WHERE IDFRPD = @Id;
+
+SELECT @@ROWCOUNT;";
+
+ try
+ {
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ {
+ connection.Open();
+ var blockers = ReferenceDirectorySqlHelpers.LoadDeleteBlockersFromForeignKeys(connection, "FRPD", id);
+ if (blockers.Count > 0)
+ {
+ return new DirectoryDeleteResult
+ {
+ IsDeleted = false,
+ WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPD", blockers)
+ };
+ }
+
+ using (var command = new SqlCommand(sql, connection))
+ {
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
+ if (Convert.ToInt32(command.ExecuteScalar()) == 0)
+ {
+ throw new InvalidOperationException("Запись FRPD для удаления не найдена.");
+ }
+ }
+
+ return new DirectoryDeleteResult
+ {
+ IsDeleted = true
+ };
+ }
+ }
+ catch (SqlException ex) when (ex.Number == 547)
+ {
+ return new DirectoryDeleteResult
+ {
+ IsDeleted = false,
+ WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPD", ex)
+ };
+ }
+ }
+
+ public DirectoryDeleteResult DeleteFrpdvdItem(int id)
+ {
+ if (id <= 0)
+ {
+ throw new InvalidOperationException("Не выбрана запись FRPDVD для удаления.");
+ }
+
+ const string sql = @"
+DELETE FROM dbo.FRPDVD
+WHERE IDFRPDVD = @Id;
+
+SELECT @@ROWCOUNT;";
+
+ try
+ {
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ {
+ connection.Open();
+ var blockers = ReferenceDirectorySqlHelpers.LoadDeleteBlockersFromForeignKeys(connection, "FRPDVD", id);
+ if (blockers.Count > 0)
+ {
+ return new DirectoryDeleteResult
+ {
+ IsDeleted = false,
+ WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPDVD", blockers)
+ };
+ }
+
+ using (var command = new SqlCommand(sql, connection))
+ {
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
+ if (Convert.ToInt32(command.ExecuteScalar()) == 0)
+ {
+ throw new InvalidOperationException("Запись FRPDVD для удаления не найдена.");
+ }
+ }
+
+ return new DirectoryDeleteResult
+ {
+ IsDeleted = true
+ };
+ }
+ }
+ catch (SqlException ex) when (ex.Number == 547)
+ {
+ return new DirectoryDeleteResult
+ {
+ IsDeleted = false,
+ WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPDVD", ex)
+ };
+ }
+ }
+
+ public IReadOnlyList LoadFrpdItems()
+ {
+ const string sql = @"
+SELECT
+ fr.IDFRPD AS Id,
+ fr.IDFRPDR AS ParentId,
+ parent.NMFRPD AS ParentName,
+ fr.NMFRPD AS Name,
+ fr.KDFRPDLC AS LocalCode,
+ fr.FRPDGUID AS Guid,
+ fr.DTSZFRPD AS CreatedOn,
+ fr.DTLKFRPD AS LiquidatedOn,
+ ISNULL(
+ STUFF((
+ SELECT ', ' + activity.ActivityName
+ FROM
+ (
+ SELECT DISTINCT sp.NMVDDO AS ActivityName
+ FROM dbo.FRPDVD link
+ JOIN dbo.SPVDDO sp ON sp.IDSPVDDO = link.IDSPVDDO
+ WHERE link.IDFRPD = fr.IDFRPD
+ ) activity
+ ORDER BY activity.ActivityName
+ FOR XML PATH(''), TYPE
+ ).value('.', 'nvarchar(max)'), 1, 2, ''),
+ ''
+ ) AS ActivityNames
+FROM dbo.FRPD fr
+LEFT JOIN dbo.FRPD parent ON parent.IDFRPD = fr.IDFRPDR
+ORDER BY fr.NMFRPD, fr.IDFRPD;";
+
+ var items = new List();
+
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ using (var command = new SqlCommand(sql, connection))
+ {
+ connection.Open();
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+
+ using (var reader = command.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ items.Add(new FrpdDirectoryItem
+ {
+ ActivityNames = ReferenceDirectorySqlHelpers.GetString(reader, "ActivityNames"),
+ CreatedOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "CreatedOn"),
+ Guid = ReferenceDirectorySqlHelpers.GetString(reader, "Guid"),
+ Id = ReferenceDirectorySqlHelpers.GetInt32(reader, "Id"),
+ LiquidatedOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "LiquidatedOn"),
+ LocalCode = ReferenceDirectorySqlHelpers.GetString(reader, "LocalCode"),
+ Name = ReferenceDirectorySqlHelpers.GetString(reader, "Name"),
+ ParentId = ReferenceDirectorySqlHelpers.GetNullableInt32(reader, "ParentId"),
+ ParentName = ReferenceDirectorySqlHelpers.GetString(reader, "ParentName")
+ });
+ }
+ }
+ }
+
+ return items;
+ }
+
+ public IReadOnlyList LoadFrpdReferences()
+ {
+ return ReferenceDirectorySqlHelpers.LoadLookupItems(@"
+SELECT
+ fr.IDFRPD AS Id,
+ fr.NMFRPD AS Name
+FROM dbo.FRPD fr
+ORDER BY fr.NMFRPD, fr.IDFRPD;");
+ }
+
+ public IReadOnlyList LoadFrpdvdItems(int frpdId)
+ {
+ const string sql = @"
+SELECT
+ link.IDFRPDVD AS Id,
+ link.IDFRPD AS FrpdId,
+ link.IDSPVDDO AS ActivityId,
+ sp.NMVDDO AS ActivityName
+FROM dbo.FRPDVD link
+JOIN dbo.SPVDDO sp ON sp.IDSPVDDO = link.IDSPVDDO
+WHERE link.IDFRPD = @FrpdId
+ORDER BY sp.NMVDDO, link.IDFRPDVD;";
+
+ var items = new List();
+
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ using (var command = new SqlCommand(sql, connection))
+ {
+ connection.Open();
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@FrpdId", SqlDbType.Int).Value = frpdId;
+
+ using (var reader = command.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ items.Add(new FrpdvdDirectoryItem
+ {
+ ActivityId = ReferenceDirectorySqlHelpers.GetInt32(reader, "ActivityId"),
+ ActivityName = ReferenceDirectorySqlHelpers.GetString(reader, "ActivityName"),
+ FrpdId = ReferenceDirectorySqlHelpers.GetInt32(reader, "FrpdId"),
+ Id = ReferenceDirectorySqlHelpers.GetInt32(reader, "Id")
+ });
+ }
+ }
+ }
+
+ return items;
+ }
+
+ public IReadOnlyList LoadSpvddoReferences()
+ {
+ return ReferenceDirectorySqlHelpers.LoadLookupItems(@"
+SELECT
+ sp.IDSPVDDO AS Id,
+ sp.NMVDDO AS Name
+FROM dbo.SPVDDO sp
+ORDER BY sp.NMVDDO, sp.IDSPVDDO;");
+ }
+
+ public void UpdateFrpdItem(FrpdDirectoryItem item)
+ {
+ var normalizedItem = NormalizeFrpdItem(item);
+ if (normalizedItem.Id <= 0)
+ {
+ throw new InvalidOperationException("Не выбрана запись FRPD для изменения.");
+ }
+
+ const string sql = @"
+UPDATE dbo.FRPD
+SET IDFRPDR = @ParentId,
+ NMFRPD = @Name,
+ KDFRPDLC = @LocalCode,
+ FRPDGUID = @Guid,
+ DTSZFRPD = @CreatedOn,
+ DTLKFRPD = @LiquidatedOn
+WHERE IDFRPD = @Id;
+
+SELECT @@ROWCOUNT;";
+
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ using (var command = new SqlCommand(sql, connection))
+ {
+ connection.Open();
+ EnsureFrpdGuidIsUnique(connection, normalizedItem.Guid, normalizedItem.Id);
+
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@Id", SqlDbType.Int).Value = normalizedItem.Id;
+ ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ParentId", normalizedItem.ParentId);
+ command.Parameters.Add("@Name", SqlDbType.VarChar, FrpdDirectoryRules.NameMaxLength).Value = normalizedItem.Name;
+ ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@LocalCode", SqlDbType.VarChar, FrpdDirectoryRules.LocalCodeMaxLength, normalizedItem.LocalCode);
+ ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@Guid", SqlDbType.VarChar, FrpdDirectoryRules.GuidMaxLength, normalizedItem.Guid);
+ ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@CreatedOn", normalizedItem.CreatedOn);
+ ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@LiquidatedOn", normalizedItem.LiquidatedOn);
+
+ try
+ {
+ if (Convert.ToInt32(command.ExecuteScalar()) == 0)
+ {
+ throw new InvalidOperationException("Запись FRPD для изменения не найдена.");
+ }
+ }
+ catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "IX_FRPD_FRPDGUID"))
+ {
+ throw CreateFrpdDuplicateGuidException(normalizedItem.Guid);
+ }
+ }
+ }
+
+ public void UpdateFrpdvdItem(FrpdvdDirectoryItem item)
+ {
+ var normalizedItem = NormalizeFrpdvdItem(item);
+ if (normalizedItem.Id <= 0)
+ {
+ throw new InvalidOperationException("Не выбрана запись FRPDVD для изменения.");
+ }
+
+ const string sql = @"
+UPDATE dbo.FRPDVD
+SET IDSPVDDO = @ActivityId
+WHERE IDFRPDVD = @Id;
+
+SELECT @@ROWCOUNT;";
+
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ using (var command = new SqlCommand(sql, connection))
+ {
+ connection.Open();
+ EnsureFrpdvdIsUnique(connection, normalizedItem.FrpdId, normalizedItem.ActivityId, normalizedItem.Id);
+
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@Id", SqlDbType.Int).Value = normalizedItem.Id;
+ command.Parameters.Add("@ActivityId", SqlDbType.Int).Value = normalizedItem.ActivityId;
+
+ try
+ {
+ if (Convert.ToInt32(command.ExecuteScalar()) == 0)
+ {
+ throw new InvalidOperationException("Запись FRPDVD для изменения не найдена.");
+ }
+ }
+ catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "XAK1FRPDVD"))
+ {
+ throw CreateFrpdvdDuplicateException();
+ }
+ }
+ }
+
+ private static InvalidOperationException CreateFrpdDuplicateGuidException(string guid)
+ {
+ return new InvalidOperationException(string.Format("GUID подразделения \"{0}\" уже существует в справочнике.", guid));
+ }
+
+ private static InvalidOperationException CreateFrpdvdDuplicateException()
+ {
+ return new InvalidOperationException("Выбранный вид деятельности уже существует у этой организации/подразделения.");
+ }
+
+ private static void EnsureFrpdGuidIsUnique(SqlConnection connection, string guid, int? excludeId)
+ {
+ if (string.IsNullOrWhiteSpace(guid))
+ {
+ return;
+ }
+
+ const string sql = @"
+SELECT COUNT(1)
+FROM dbo.FRPD
+WHERE FRPDGUID = @Guid
+ AND (@ExcludeId IS NULL OR IDFRPD <> @ExcludeId);";
+
+ using (var command = new SqlCommand(sql, connection))
+ {
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@Guid", SqlDbType.VarChar, FrpdDirectoryRules.GuidMaxLength).Value = guid;
+ ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ExcludeId", excludeId);
+ if (Convert.ToInt32(command.ExecuteScalar()) > 0)
+ {
+ throw CreateFrpdDuplicateGuidException(guid);
+ }
+ }
+ }
+
+ private static void EnsureFrpdvdIsUnique(SqlConnection connection, int frpdId, int activityId, int? excludeId)
+ {
+ const string sql = @"
+SELECT COUNT(1)
+FROM dbo.FRPDVD
+WHERE IDFRPD = @FrpdId
+ AND IDSPVDDO = @ActivityId
+ AND (@ExcludeId IS NULL OR IDFRPDVD <> @ExcludeId);";
+
+ using (var command = new SqlCommand(sql, connection))
+ {
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@FrpdId", SqlDbType.Int).Value = frpdId;
+ command.Parameters.Add("@ActivityId", SqlDbType.Int).Value = activityId;
+ ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ExcludeId", excludeId);
+ if (Convert.ToInt32(command.ExecuteScalar()) > 0)
+ {
+ throw CreateFrpdvdDuplicateException();
+ }
+ }
+ }
+
+ private static string NormalizeRequired(string value, int maxLength, string fieldDisplayName)
+ {
+ var normalizedValue = string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
+ if (normalizedValue.Length == 0)
+ {
+ throw new InvalidOperationException(string.Format("Укажите {0}.", fieldDisplayName));
+ }
+
+ if (normalizedValue.Length > maxLength)
+ {
+ throw new InvalidOperationException(string.Format("{0} не должно превышать {1} символов.", fieldDisplayName, maxLength));
+ }
+
+ return normalizedValue;
+ }
+
+ private static string NormalizeNullable(string value, int maxLength, string fieldDisplayName)
+ {
+ var normalizedValue = string.IsNullOrWhiteSpace(value) ? null : value.Trim();
+ if (normalizedValue != null && normalizedValue.Length > maxLength)
+ {
+ throw new InvalidOperationException(string.Format("{0} не должно превышать {1} символов.", fieldDisplayName, maxLength));
+ }
+
+ return normalizedValue;
+ }
+
+ private static FrpdDirectoryItem NormalizeFrpdItem(FrpdDirectoryItem item)
+ {
+ if (item == null)
+ {
+ throw new InvalidOperationException("Не передана запись FRPD.");
+ }
+
+ var normalizedItem = new FrpdDirectoryItem
+ {
+ CreatedOn = item.CreatedOn,
+ Guid = NormalizeNullable(item.Guid, FrpdDirectoryRules.GuidMaxLength, "GUID подразделения"),
+ Id = item.Id,
+ LiquidatedOn = item.LiquidatedOn,
+ LocalCode = NormalizeNullable(item.LocalCode, FrpdDirectoryRules.LocalCodeMaxLength, "Локальный код организации/подразделения"),
+ Name = NormalizeRequired(item.Name, FrpdDirectoryRules.NameMaxLength, "организацию/подразделение"),
+ ParentId = item.ParentId.HasValue && item.ParentId.Value > 0 ? item.ParentId : null
+ };
+
+ if (normalizedItem.Id > 0
+ && normalizedItem.ParentId.HasValue
+ && normalizedItem.ParentId.Value == normalizedItem.Id)
+ {
+ throw new InvalidOperationException("Организация/подразделение не может ссылаться на себя как на родительскую запись.");
+ }
+
+ return normalizedItem;
+ }
+
+ private static FrpdvdDirectoryItem NormalizeFrpdvdItem(FrpdvdDirectoryItem item)
+ {
+ if (item == null)
+ {
+ throw new InvalidOperationException("Не передана запись FRPDVD.");
+ }
+
+ if (item.FrpdId <= 0)
+ {
+ throw new InvalidOperationException("Не выбрана организация/подразделение для вида деятельности.");
+ }
+
+ if (item.ActivityId <= 0)
+ {
+ throw new InvalidOperationException("Укажите вид деятельности организации.");
+ }
+
+ return new FrpdvdDirectoryItem
+ {
+ ActivityId = item.ActivityId,
+ FrpdId = item.FrpdId,
+ Id = item.Id
+ };
+ }
+ }
+}
+
diff --git a/XLAB2/FrpdDirectoryWindow.xaml b/XLAB2/FrpdDirectoryWindow.xaml
new file mode 100644
index 0000000..61876c1
--- /dev/null
+++ b/XLAB2/FrpdDirectoryWindow.xaml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XLAB2/FrpdDirectoryWindow.xaml.cs b/XLAB2/FrpdDirectoryWindow.xaml.cs
new file mode 100644
index 0000000..a58e98e
--- /dev/null
+++ b/XLAB2/FrpdDirectoryWindow.xaml.cs
@@ -0,0 +1,33 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace XLAB2
+{
+ public partial class FrpdDirectoryWindow : Window
+ {
+ private readonly FrpdDirectoryWindowViewModel _viewModel;
+
+ public FrpdDirectoryWindow()
+ {
+ InitializeComponent();
+ _viewModel = new FrpdDirectoryWindowViewModel(new FrpdDirectoryService(), new FrpdDirectoryDialogService(this));
+ DataContext = _viewModel;
+ }
+
+ private void DataGridRow_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ var row = sender as DataGridRow;
+ if (row != null)
+ {
+ row.IsSelected = true;
+ row.Focus();
+ }
+ }
+
+ private async void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ await _viewModel.InitializeAsync();
+ }
+ }
+}
diff --git a/XLAB2/FrpdDirectoryWindowViewModel.cs b/XLAB2/FrpdDirectoryWindowViewModel.cs
new file mode 100644
index 0000000..aac455b
--- /dev/null
+++ b/XLAB2/FrpdDirectoryWindowViewModel.cs
@@ -0,0 +1,476 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Input;
+
+namespace XLAB2
+{
+ internal sealed class FrpdDirectoryWindowViewModel : ObservableObject
+ {
+ private readonly IFrpdDirectoryDialogService _dialogService;
+ private readonly FrpdDirectoryService _service;
+ private List _frpdCache;
+ private bool _isBusy;
+ private string _searchText;
+ private FrpdDirectoryItem _selectedFrpd;
+ private FrpdvdDirectoryItem _selectedFrpdvd;
+ private string _statusText;
+
+ public FrpdDirectoryWindowViewModel(FrpdDirectoryService service, IFrpdDirectoryDialogService dialogService)
+ {
+ _service = service;
+ _dialogService = dialogService;
+ _frpdCache = new List();
+
+ FrpdItems = new ObservableCollection();
+ FrpdvdItems = new ObservableCollection();
+
+ AddFrpdCommand = new RelayCommand(delegate { AddFrpdAsync(); }, delegate { return !IsBusy; });
+ EditFrpdCommand = new RelayCommand(delegate { EditFrpdAsync(); }, delegate { return !IsBusy && SelectedFrpd != null; });
+ DeleteFrpdCommand = new RelayCommand(delegate { DeleteFrpdAsync(); }, delegate { return !IsBusy && SelectedFrpd != null; });
+ AddFrpdvdCommand = new RelayCommand(delegate { AddFrpdvdAsync(); }, delegate { return !IsBusy && SelectedFrpd != null; });
+ EditFrpdvdCommand = new RelayCommand(delegate { EditFrpdvdAsync(); }, delegate { return !IsBusy && SelectedFrpdvd != null; });
+ DeleteFrpdvdCommand = new RelayCommand(delegate { DeleteFrpdvdAsync(); }, delegate { return !IsBusy && SelectedFrpdvd != null; });
+ RefreshCommand = new RelayCommand(delegate { RefreshAsync(); }, delegate { return !IsBusy; });
+
+ UpdateStatus();
+ }
+
+ public ICommand AddFrpdCommand { get; private set; }
+ public ICommand AddFrpdvdCommand { get; private set; }
+ public ICommand DeleteFrpdCommand { get; private set; }
+ public ICommand DeleteFrpdvdCommand { get; private set; }
+ public ICommand EditFrpdCommand { get; private set; }
+ public ICommand EditFrpdvdCommand { get; private set; }
+ public ObservableCollection FrpdItems { get; private set; }
+ public ObservableCollection FrpdvdItems { get; private set; }
+ public ICommand RefreshCommand { get; private set; }
+
+ public bool IsBusy
+ {
+ get { return _isBusy; }
+ private set
+ {
+ if (SetProperty(ref _isBusy, value))
+ {
+ RaiseCommandStates();
+ }
+ }
+ }
+
+ public string SearchText
+ {
+ get { return _searchText; }
+ set
+ {
+ if (SetProperty(ref _searchText, value))
+ {
+ ApplySearchFilter();
+ UpdateStatus();
+ }
+ }
+ }
+
+ public FrpdDirectoryItem SelectedFrpd
+ {
+ get { return _selectedFrpd; }
+ set
+ {
+ if (SetProperty(ref _selectedFrpd, value))
+ {
+ RaiseCommandStates();
+ LoadFrpdvdForSelection();
+ UpdateStatus();
+ }
+ }
+ }
+
+ public FrpdvdDirectoryItem SelectedFrpdvd
+ {
+ get { return _selectedFrpdvd; }
+ set
+ {
+ if (SetProperty(ref _selectedFrpdvd, value))
+ {
+ RaiseCommandStates();
+ UpdateStatus();
+ }
+ }
+ }
+
+ public string StatusText
+ {
+ get { return _statusText; }
+ private set { SetProperty(ref _statusText, value); }
+ }
+
+ public async Task InitializeAsync()
+ {
+ await ExecuteBusyOperationAsync(async delegate { await RefreshFrpdCoreAsync(null, null); });
+ }
+
+ private void AddFrpdAsync()
+ {
+ var result = _dialogService.ShowFrpdEditDialog(new FrpdDirectoryItem(), true, _frpdCache.ToList(), _service);
+ if (result == null)
+ {
+ return;
+ }
+
+ RunMutationOperation(async delegate
+ {
+ var createdId = await Task.Run(delegate { return _service.AddFrpdItem(result); });
+ await RefreshFrpdCoreAsync(createdId, null);
+ _dialogService.ShowInfo("Запись FRPD добавлена.");
+ });
+ }
+
+ private void AddFrpdvdAsync()
+ {
+ if (SelectedFrpd == null)
+ {
+ return;
+ }
+
+ var result = _dialogService.ShowFrpdvdEditDialog(new FrpdvdDirectoryItem { FrpdId = SelectedFrpd.Id }, true, FrpdvdItems.ToList(), _service);
+ if (result == null)
+ {
+ return;
+ }
+
+ RunMutationOperation(async delegate
+ {
+ var createdId = await Task.Run(delegate { return _service.AddFrpdvdItem(result); });
+ await RefreshFrpdCoreAsync(result.FrpdId, createdId);
+ _dialogService.ShowInfo("Запись FRPDVD добавлена.");
+ });
+ }
+
+ private void ApplyFrpdFilter(int? preferredId)
+ {
+ var filteredItems = _frpdCache.Where(delegate(FrpdDirectoryItem item) { return MatchesSearch(item); }).ToList();
+ FrpdItems.Clear();
+ foreach (var item in filteredItems)
+ {
+ FrpdItems.Add(item);
+ }
+
+ SelectedFrpd = preferredId.HasValue
+ ? FrpdItems.FirstOrDefault(delegate(FrpdDirectoryItem item) { return item.Id == preferredId.Value; })
+ : FrpdItems.FirstOrDefault();
+ }
+
+ private void ApplySearchFilter()
+ {
+ if (!IsBusy)
+ {
+ ApplyFrpdFilter(SelectedFrpd == null ? (int?)null : SelectedFrpd.Id);
+ }
+ }
+
+ private static FrpdDirectoryItem CloneFrpd(FrpdDirectoryItem source)
+ {
+ return new FrpdDirectoryItem
+ {
+ CreatedOn = source.CreatedOn,
+ Guid = source.Guid,
+ Id = source.Id,
+ LiquidatedOn = source.LiquidatedOn,
+ LocalCode = source.LocalCode,
+ Name = source.Name,
+ ParentId = source.ParentId,
+ ParentName = source.ParentName
+ };
+ }
+
+ private static FrpdvdDirectoryItem CloneFrpdvd(FrpdvdDirectoryItem source)
+ {
+ return new FrpdvdDirectoryItem
+ {
+ ActivityId = source.ActivityId,
+ ActivityName = source.ActivityName,
+ FrpdId = source.FrpdId,
+ Id = source.Id
+ };
+ }
+
+ private void DeleteFrpdAsync()
+ {
+ if (SelectedFrpd == null)
+ {
+ return;
+ }
+
+ var selected = SelectedFrpd;
+ if (!_dialogService.Confirm(string.Format("Удалить организацию/подразделение \"{0}\"?", selected.Name)))
+ {
+ return;
+ }
+
+ RunMutationOperation(async delegate
+ {
+ var result = await Task.Run(delegate { return _service.DeleteFrpdItem(selected.Id); });
+ if (!result.IsDeleted)
+ {
+ _dialogService.ShowWarning(result.WarningMessage);
+ return;
+ }
+
+ await RefreshFrpdCoreAsync(null, null);
+ _dialogService.ShowInfo("Запись удалена.");
+ });
+ }
+
+ private void DeleteFrpdvdAsync()
+ {
+ if (SelectedFrpdvd == null)
+ {
+ return;
+ }
+
+ var selected = SelectedFrpdvd;
+ if (!_dialogService.Confirm(string.Format("Удалить вид деятельности \"{0}\"?", selected.ActivityName)))
+ {
+ return;
+ }
+
+ RunMutationOperation(async delegate
+ {
+ var result = await Task.Run(delegate { return _service.DeleteFrpdvdItem(selected.Id); });
+ if (!result.IsDeleted)
+ {
+ _dialogService.ShowWarning(result.WarningMessage);
+ return;
+ }
+
+ await RefreshFrpdCoreAsync(selected.FrpdId, null);
+ _dialogService.ShowInfo("Запись удалена.");
+ });
+ }
+
+ private async Task ExecuteBusyOperationAsync(Func operation)
+ {
+ try
+ {
+ IsBusy = true;
+ await operation();
+ }
+ catch (Exception ex)
+ {
+ _dialogService.ShowError(ex.Message);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task ExecuteMutationOperationAsync(Func operation)
+ {
+ try
+ {
+ IsBusy = true;
+ await operation();
+ }
+ catch (InvalidOperationException ex)
+ {
+ _dialogService.ShowWarning(ex.Message);
+ }
+ catch (Exception ex)
+ {
+ _dialogService.ShowError(ex.Message);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private void EditFrpdAsync()
+ {
+ if (SelectedFrpd == null)
+ {
+ return;
+ }
+
+ var result = _dialogService.ShowFrpdEditDialog(CloneFrpd(SelectedFrpd), false, _frpdCache.ToList(), _service);
+ if (result == null)
+ {
+ return;
+ }
+
+ RunMutationOperation(async delegate
+ {
+ await Task.Run(delegate { _service.UpdateFrpdItem(result); });
+ await RefreshFrpdCoreAsync(result.Id, SelectedFrpdvd == null ? (int?)null : SelectedFrpdvd.Id);
+ _dialogService.ShowInfo("Запись обновлена.");
+ });
+ }
+
+ private void EditFrpdvdAsync()
+ {
+ if (SelectedFrpdvd == null)
+ {
+ return;
+ }
+
+ var result = _dialogService.ShowFrpdvdEditDialog(CloneFrpdvd(SelectedFrpdvd), false, FrpdvdItems.ToList(), _service);
+ if (result == null)
+ {
+ return;
+ }
+
+ RunMutationOperation(async delegate
+ {
+ await Task.Run(delegate { _service.UpdateFrpdvdItem(result); });
+ await RefreshFrpdCoreAsync(result.FrpdId, result.Id);
+ _dialogService.ShowInfo("Запись обновлена.");
+ });
+ }
+
+ private string[] GetSearchTokens()
+ {
+ return (SearchText ?? string.Empty)
+ .Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(delegate(string token) { return token.Trim().ToUpperInvariant(); })
+ .Where(delegate(string token) { return token.Length > 0; })
+ .ToArray();
+ }
+
+ private void LoadFrpdvdForSelection()
+ {
+ if (!IsBusy)
+ {
+ RunBusyOperation(async delegate
+ {
+ if (SelectedFrpd == null)
+ {
+ FrpdvdItems.Clear();
+ SelectedFrpdvd = null;
+ UpdateStatus();
+ return;
+ }
+
+ await RefreshFrpdvdCoreAsync(SelectedFrpd.Id, null);
+ });
+ }
+ }
+
+ private bool MatchesSearch(FrpdDirectoryItem item)
+ {
+ if (item == null)
+ {
+ return false;
+ }
+
+ var tokens = GetSearchTokens();
+ if (tokens.Length == 0)
+ {
+ return true;
+ }
+
+ var haystack = string.Join(
+ " ",
+ new[]
+ {
+ item.Id.ToString(),
+ item.Name,
+ item.ParentName,
+ item.LocalCode,
+ item.Guid,
+ item.ActivityNames
+ }
+ .Where(delegate(string value) { return !string.IsNullOrWhiteSpace(value); }))
+ .ToUpperInvariant();
+ return tokens.All(delegate(string token) { return haystack.IndexOf(token, StringComparison.Ordinal) >= 0; });
+ }
+
+ private async Task RefreshFrpdCoreAsync(int? frpdIdToSelect, int? frpdvdIdToSelect)
+ {
+ var currentFrpdId = SelectedFrpd == null ? (int?)null : SelectedFrpd.Id;
+ _frpdCache = (await Task.Run(delegate { return _service.LoadFrpdItems(); })).ToList();
+
+ ApplyFrpdFilter(frpdIdToSelect.HasValue ? frpdIdToSelect : currentFrpdId);
+ if (SelectedFrpd == null)
+ {
+ FrpdvdItems.Clear();
+ SelectedFrpdvd = null;
+ UpdateStatus();
+ return;
+ }
+
+ await RefreshFrpdvdCoreAsync(SelectedFrpd.Id, frpdvdIdToSelect);
+ UpdateStatus();
+ }
+
+ private async Task RefreshFrpdvdCoreAsync(int frpdId, int? frpdvdIdToSelect)
+ {
+ var items = await Task.Run(delegate { return _service.LoadFrpdvdItems(frpdId); });
+ FrpdvdItems.Clear();
+ foreach (var item in items)
+ {
+ FrpdvdItems.Add(item);
+ }
+
+ SelectedFrpdvd = frpdvdIdToSelect.HasValue
+ ? FrpdvdItems.FirstOrDefault(delegate(FrpdvdDirectoryItem item) { return item.Id == frpdvdIdToSelect.Value; })
+ : FrpdvdItems.FirstOrDefault();
+ UpdateStatus();
+ }
+
+ private void RaiseCommandStates()
+ {
+ ((RelayCommand)AddFrpdCommand).RaiseCanExecuteChanged();
+ ((RelayCommand)EditFrpdCommand).RaiseCanExecuteChanged();
+ ((RelayCommand)DeleteFrpdCommand).RaiseCanExecuteChanged();
+ ((RelayCommand)AddFrpdvdCommand).RaiseCanExecuteChanged();
+ ((RelayCommand)EditFrpdvdCommand).RaiseCanExecuteChanged();
+ ((RelayCommand)DeleteFrpdvdCommand).RaiseCanExecuteChanged();
+ ((RelayCommand)RefreshCommand).RaiseCanExecuteChanged();
+ }
+
+ private void RefreshAsync()
+ {
+ RunBusyOperation(async delegate { await RefreshFrpdCoreAsync(SelectedFrpd == null ? (int?)null : SelectedFrpd.Id, SelectedFrpdvd == null ? (int?)null : SelectedFrpdvd.Id); });
+ }
+
+ private async void RunBusyOperation(Func operation)
+ {
+ try
+ {
+ await ExecuteBusyOperationAsync(operation);
+ }
+ catch (Exception ex)
+ {
+ IsBusy = false;
+ _dialogService.ShowError(ex.Message);
+ }
+ }
+
+ private async void RunMutationOperation(Func operation)
+ {
+ try
+ {
+ await ExecuteMutationOperationAsync(operation);
+ }
+ catch (Exception ex)
+ {
+ IsBusy = false;
+ _dialogService.ShowError(ex.Message);
+ }
+ }
+
+ private void UpdateStatus()
+ {
+ var searchText = string.IsNullOrWhiteSpace(SearchText) ? null : SearchText.Trim();
+ StatusText = string.Format(
+ "{0}Подразделений: {1}/{2}. Видов деятельности: {3}.",
+ string.IsNullOrWhiteSpace(searchText) ? string.Empty : string.Format("Поиск: \"{0}\". ", searchText),
+ FrpdItems.Count,
+ _frpdCache.Count,
+ FrpdvdItems.Count);
+ }
+ }
+}
diff --git a/XLAB2/FrpdEditWindow.xaml b/XLAB2/FrpdEditWindow.xaml
new file mode 100644
index 0000000..17ac697
--- /dev/null
+++ b/XLAB2/FrpdEditWindow.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XLAB2/FrpdEditWindow.xaml.cs b/XLAB2/FrpdEditWindow.xaml.cs
new file mode 100644
index 0000000..d481707
--- /dev/null
+++ b/XLAB2/FrpdEditWindow.xaml.cs
@@ -0,0 +1,20 @@
+using System.Windows;
+
+namespace XLAB2
+{
+ public partial class FrpdEditWindow : Window
+ {
+ internal FrpdEditWindow(FrpdEditWindowViewModel viewModel)
+ {
+ InitializeComponent();
+ DataContext = viewModel;
+ viewModel.CloseRequested += ViewModelOnCloseRequested;
+ }
+
+ private void ViewModelOnCloseRequested(object sender, bool? dialogResult)
+ {
+ DialogResult = dialogResult;
+ Close();
+ }
+ }
+}
diff --git a/XLAB2/FrpdEditWindowViewModel.cs b/XLAB2/FrpdEditWindowViewModel.cs
new file mode 100644
index 0000000..0ea1592
--- /dev/null
+++ b/XLAB2/FrpdEditWindowViewModel.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Input;
+
+namespace XLAB2
+{
+ internal sealed class FrpdEditWindowViewModel : ObservableObject
+ {
+ private readonly IReadOnlyList _existingItems;
+ private string _guid;
+ private string _localCode;
+ private string _name;
+ private int _parentId;
+ private string _validationMessage;
+
+ public FrpdEditWindowViewModel(FrpdDirectoryItem seed, bool isNew, IReadOnlyList existingItems, FrpdDirectoryService service)
+ {
+ var source = seed ?? new FrpdDirectoryItem();
+ _existingItems = existingItems ?? Array.Empty();
+ Id = source.Id;
+ IsNew = isNew;
+
+ ParentItems = new[] { new DirectoryLookupItem { Id = 0, Name = string.Empty } }
+ .Concat((service.LoadFrpdReferences() ?? Array.Empty()).Where(delegate(DirectoryLookupItem item) { return item.Id != Id; }))
+ .ToList();
+
+ ParentId = source.ParentId.HasValue ? source.ParentId.Value : 0;
+ Name = source.Name ?? string.Empty;
+ LocalCode = source.LocalCode ?? string.Empty;
+ Guid = source.Guid ?? string.Empty;
+ CreatedOn = source.CreatedOn;
+ LiquidatedOn = source.LiquidatedOn;
+
+ ConfirmCommand = new RelayCommand(Confirm);
+ CancelCommand = new RelayCommand(Cancel);
+ }
+
+ public event EventHandler CloseRequested;
+
+ public ICommand CancelCommand { get; private set; }
+ public ICommand ConfirmCommand { get; private set; }
+ public DateTime? CreatedOn { get; set; }
+
+ public string Guid
+ {
+ get { return _guid; }
+ set { SetProperty(ref _guid, value); }
+ }
+
+ public Visibility GuidFieldVisibility
+ {
+ get { return IsNew ? Visibility.Collapsed : Visibility.Visible; }
+ }
+
+ public int Id { get; private set; }
+ public bool IsNew { get; private set; }
+ public DateTime? LiquidatedOn { get; set; }
+
+ public string LocalCode
+ {
+ get { return _localCode; }
+ set { SetProperty(ref _localCode, value); }
+ }
+
+ public string Name
+ {
+ get { return _name; }
+ set { SetProperty(ref _name, value); }
+ }
+
+ public int ParentId
+ {
+ get { return _parentId; }
+ set { SetProperty(ref _parentId, value); }
+ }
+
+ public IReadOnlyList ParentItems { get; private set; }
+
+ public string Title
+ {
+ get { return IsNew ? "Новая организация/подразделение" : "Редактирование организации/подразделения"; }
+ }
+
+ public string ValidationMessage
+ {
+ get { return _validationMessage; }
+ private set { SetProperty(ref _validationMessage, value); }
+ }
+
+ public FrpdDirectoryItem ToResult()
+ {
+ return new FrpdDirectoryItem
+ {
+ CreatedOn = CreatedOn,
+ Guid = string.IsNullOrWhiteSpace(Guid) ? null : Guid.Trim(),
+ Id = Id,
+ LiquidatedOn = LiquidatedOn,
+ LocalCode = string.IsNullOrWhiteSpace(LocalCode) ? null : LocalCode.Trim(),
+ Name = string.IsNullOrWhiteSpace(Name) ? string.Empty : Name.Trim(),
+ ParentId = ParentId > 0 ? (int?)ParentId : null
+ };
+ }
+
+ private void Cancel(object parameter)
+ {
+ RaiseCloseRequested(false);
+ }
+
+ private void Confirm(object parameter)
+ {
+ var normalizedName = string.IsNullOrWhiteSpace(Name) ? string.Empty : Name.Trim();
+ var normalizedLocalCode = string.IsNullOrWhiteSpace(LocalCode) ? null : LocalCode.Trim();
+ var normalizedGuid = string.IsNullOrWhiteSpace(Guid) ? null : Guid.Trim();
+
+ if (normalizedName.Length == 0)
+ {
+ ValidationMessage = "Укажите организацию/подразделение.";
+ return;
+ }
+
+ if (normalizedName.Length > FrpdDirectoryRules.NameMaxLength)
+ {
+ ValidationMessage = string.Format("Организация/подразделение не должна превышать {0} символов.", FrpdDirectoryRules.NameMaxLength);
+ return;
+ }
+
+ if (normalizedLocalCode != null && normalizedLocalCode.Length > FrpdDirectoryRules.LocalCodeMaxLength)
+ {
+ ValidationMessage = string.Format("Локальный код не должен превышать {0} символов.", FrpdDirectoryRules.LocalCodeMaxLength);
+ return;
+ }
+
+ if (normalizedGuid != null && normalizedGuid.Length > FrpdDirectoryRules.GuidMaxLength)
+ {
+ ValidationMessage = string.Format("GUID подразделения не должен превышать {0} символов.", FrpdDirectoryRules.GuidMaxLength);
+ return;
+ }
+
+ if (ParentId == Id && Id > 0)
+ {
+ ValidationMessage = "Нельзя выбрать эту же запись как родительскую.";
+ return;
+ }
+
+ var duplicateGuid = _existingItems.FirstOrDefault(delegate(FrpdDirectoryItem item)
+ {
+ return item != null
+ && item.Id != Id
+ && !string.IsNullOrWhiteSpace(item.Guid)
+ && normalizedGuid != null
+ && string.Equals(item.Guid.Trim(), normalizedGuid, StringComparison.OrdinalIgnoreCase);
+ });
+ if (duplicateGuid != null)
+ {
+ ValidationMessage = string.Format("GUID подразделения \"{0}\" уже существует в справочнике.", normalizedGuid);
+ return;
+ }
+
+ ValidationMessage = string.Empty;
+ RaiseCloseRequested(true);
+ }
+
+ private void RaiseCloseRequested(bool? dialogResult)
+ {
+ var handler = CloseRequested;
+ if (handler != null)
+ {
+ handler(this, dialogResult);
+ }
+ }
+ }
+}
diff --git a/XLAB2/FrpdvdEditWindow.xaml b/XLAB2/FrpdvdEditWindow.xaml
new file mode 100644
index 0000000..cd7a19e
--- /dev/null
+++ b/XLAB2/FrpdvdEditWindow.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XLAB2/FrpdvdEditWindow.xaml.cs b/XLAB2/FrpdvdEditWindow.xaml.cs
new file mode 100644
index 0000000..8cb7da5
--- /dev/null
+++ b/XLAB2/FrpdvdEditWindow.xaml.cs
@@ -0,0 +1,20 @@
+using System.Windows;
+
+namespace XLAB2
+{
+ public partial class FrpdvdEditWindow : Window
+ {
+ internal FrpdvdEditWindow(FrpdvdEditWindowViewModel viewModel)
+ {
+ InitializeComponent();
+ DataContext = viewModel;
+ viewModel.CloseRequested += ViewModelOnCloseRequested;
+ }
+
+ private void ViewModelOnCloseRequested(object sender, bool? dialogResult)
+ {
+ DialogResult = dialogResult;
+ Close();
+ }
+ }
+}
diff --git a/XLAB2/FrpdvdEditWindowViewModel.cs b/XLAB2/FrpdvdEditWindowViewModel.cs
new file mode 100644
index 0000000..d8c979b
--- /dev/null
+++ b/XLAB2/FrpdvdEditWindowViewModel.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Input;
+
+namespace XLAB2
+{
+ internal sealed class FrpdvdEditWindowViewModel : ObservableObject
+ {
+ private readonly IReadOnlyList _existingItems;
+ private int _activityId;
+ private string _validationMessage;
+
+ public FrpdvdEditWindowViewModel(FrpdvdDirectoryItem seed, bool isNew, IReadOnlyList existingItems, FrpdDirectoryService service)
+ {
+ var source = seed ?? new FrpdvdDirectoryItem();
+ _existingItems = existingItems ?? Array.Empty();
+ Id = source.Id;
+ FrpdId = source.FrpdId;
+ IsNew = isNew;
+
+ ActivityItems = service.LoadSpvddoReferences();
+ ActivityId = source.ActivityId;
+
+ ConfirmCommand = new RelayCommand(Confirm);
+ CancelCommand = new RelayCommand(Cancel);
+ }
+
+ public event EventHandler CloseRequested;
+
+ public int ActivityId
+ {
+ get { return _activityId; }
+ set { SetProperty(ref _activityId, value); }
+ }
+
+ public IReadOnlyList ActivityItems { get; private set; }
+ public ICommand CancelCommand { get; private set; }
+ public ICommand ConfirmCommand { get; private set; }
+ public int FrpdId { get; private set; }
+ public int Id { get; private set; }
+ public bool IsNew { get; private set; }
+
+ public string Title
+ {
+ get { return IsNew ? "Новый вид деятельности организации" : "Редактирование вида деятельности организации"; }
+ }
+
+ public string ValidationMessage
+ {
+ get { return _validationMessage; }
+ private set { SetProperty(ref _validationMessage, value); }
+ }
+
+ public FrpdvdDirectoryItem ToResult()
+ {
+ return new FrpdvdDirectoryItem
+ {
+ ActivityId = ActivityId,
+ FrpdId = FrpdId,
+ Id = Id
+ };
+ }
+
+ private void Cancel(object parameter)
+ {
+ RaiseCloseRequested(false);
+ }
+
+ private void Confirm(object parameter)
+ {
+ if (ActivityId <= 0)
+ {
+ ValidationMessage = "Укажите вид деятельности организации.";
+ return;
+ }
+
+ var duplicate = _existingItems.FirstOrDefault(delegate(FrpdvdDirectoryItem item)
+ {
+ return item != null
+ && item.Id != Id
+ && item.ActivityId == ActivityId;
+ });
+ if (duplicate != null)
+ {
+ ValidationMessage = "Выбранный вид деятельности уже существует у этой организации/подразделения.";
+ return;
+ }
+
+ ValidationMessage = string.Empty;
+ RaiseCloseRequested(true);
+ }
+
+ private void RaiseCloseRequested(bool? dialogResult)
+ {
+ var handler = CloseRequested;
+ if (handler != null)
+ {
+ handler(this, dialogResult);
+ }
+ }
+ }
+}
diff --git a/XLAB2/Infrastructure/DatabaseConfiguration.cs b/XLAB2/Infrastructure/DatabaseConfiguration.cs
new file mode 100644
index 0000000..15def62
--- /dev/null
+++ b/XLAB2/Infrastructure/DatabaseConfiguration.cs
@@ -0,0 +1,73 @@
+using Microsoft.Extensions.Configuration;
+using System;
+using System.Threading;
+
+namespace XLAB2.Infrastructure;
+
+internal static class DatabaseConfiguration
+{
+ private static readonly Lazy Options = new(LoadOptions, LazyThreadSafetyMode.ExecutionAndPublication);
+
+ public static DatabaseOptions Current => Options.Value;
+
+ private static DatabaseOptions LoadOptions()
+ {
+ var configuration = new ConfigurationBuilder()
+ .SetBasePath(AppContext.BaseDirectory)
+ .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
+ .AddEnvironmentVariables(prefix: "XLAB2_")
+ .Build();
+
+ var options = configuration.GetSection("Database").Get() ?? new DatabaseOptions();
+ Validate(options);
+ return options;
+ }
+
+ private static void Validate(DatabaseOptions options)
+ {
+ if (string.IsNullOrWhiteSpace(options.Server))
+ {
+ throw new InvalidOperationException("Database:Server is required.");
+ }
+
+ if (string.IsNullOrWhiteSpace(options.Database))
+ {
+ throw new InvalidOperationException("Database:Database is required.");
+ }
+
+ if (options.ConnectTimeoutSeconds <= 0)
+ {
+ throw new InvalidOperationException("Database:ConnectTimeoutSeconds must be greater than zero.");
+ }
+
+ if (options.CommandTimeoutSeconds <= 0)
+ {
+ throw new InvalidOperationException("Database:CommandTimeoutSeconds must be greater than zero.");
+ }
+
+ if (options.MinPoolSize < 0)
+ {
+ throw new InvalidOperationException("Database:MinPoolSize cannot be negative.");
+ }
+
+ if (options.MaxPoolSize <= 0)
+ {
+ throw new InvalidOperationException("Database:MaxPoolSize must be greater than zero.");
+ }
+
+ if (options.MinPoolSize > options.MaxPoolSize)
+ {
+ throw new InvalidOperationException("Database:MinPoolSize cannot be greater than Database:MaxPoolSize.");
+ }
+
+ if (options.ConnectRetryCount < 0)
+ {
+ throw new InvalidOperationException("Database:ConnectRetryCount cannot be negative.");
+ }
+
+ if (options.ConnectRetryIntervalSeconds < 1)
+ {
+ throw new InvalidOperationException("Database:ConnectRetryIntervalSeconds must be greater than zero.");
+ }
+ }
+}
diff --git a/XLAB2/Infrastructure/DatabaseOptions.cs b/XLAB2/Infrastructure/DatabaseOptions.cs
new file mode 100644
index 0000000..0abdd9c
--- /dev/null
+++ b/XLAB2/Infrastructure/DatabaseOptions.cs
@@ -0,0 +1,32 @@
+namespace XLAB2.Infrastructure;
+
+internal sealed class DatabaseOptions
+{
+ public string ApplicationName { get; set; } = "XLAB2";
+
+ public int CommandTimeoutSeconds { get; set; } = 60;
+
+ public int ConnectRetryCount { get; set; } = 3;
+
+ public int ConnectRetryIntervalSeconds { get; set; } = 10;
+
+ public int ConnectTimeoutSeconds { get; set; } = 15;
+
+ public string Database { get; set; } = "ASUMS";
+
+ public bool Encrypt { get; set; } = false;
+
+ public bool IntegratedSecurity { get; set; } = true;
+
+ public bool MultipleActiveResultSets { get; set; } = true;
+
+ public bool Pooling { get; set; } = true;
+
+ public int MaxPoolSize { get; set; } = 100;
+
+ public int MinPoolSize { get; set; } = 0;
+
+ public string Server { get; set; } = @"SEVENHILL\SQLEXPRESS";
+
+ public bool TrustServerCertificate { get; set; } = true;
+}
diff --git a/XLAB2/Infrastructure/IDatabaseConnectionFactory.cs b/XLAB2/Infrastructure/IDatabaseConnectionFactory.cs
new file mode 100644
index 0000000..dde107a
--- /dev/null
+++ b/XLAB2/Infrastructure/IDatabaseConnectionFactory.cs
@@ -0,0 +1,16 @@
+using Microsoft.Data.SqlClient;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace XLAB2.Infrastructure;
+
+internal interface IDatabaseConnectionFactory
+{
+ string ConnectionString { get; }
+
+ DatabaseOptions Options { get; }
+
+ SqlConnection CreateConnection();
+
+ Task OpenConnectionAsync(CancellationToken cancellationToken = default);
+}
diff --git a/XLAB2/Infrastructure/SqlAsync.cs b/XLAB2/Infrastructure/SqlAsync.cs
new file mode 100644
index 0000000..ea88d1b
--- /dev/null
+++ b/XLAB2/Infrastructure/SqlAsync.cs
@@ -0,0 +1,172 @@
+using Microsoft.Data.SqlClient;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace XLAB2.Infrastructure;
+
+internal static class SqlAsync
+{
+ public static async Task> QueryAsync(
+ this SqlConnection connection,
+ string commandText,
+ Func map,
+ Action configureCommand = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (connection == null)
+ {
+ throw new ArgumentNullException(nameof(connection));
+ }
+
+ if (map == null)
+ {
+ throw new ArgumentNullException(nameof(map));
+ }
+
+ using var command = connection.CreateCommand();
+ command.CommandText = commandText;
+ configureCommand?.Invoke(command);
+
+ using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
+ var items = new List();
+
+ while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
+ {
+ items.Add(map(reader));
+ }
+
+ return items;
+ }
+
+ public static async Task> QueryAsync(
+ this SqlConnection connection,
+ SqlTransaction transaction,
+ string commandText,
+ Func map,
+ Action configureCommand = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (connection == null)
+ {
+ throw new ArgumentNullException(nameof(connection));
+ }
+
+ if (map == null)
+ {
+ throw new ArgumentNullException(nameof(map));
+ }
+
+ using var command = new SqlCommand(commandText, connection, transaction);
+ configureCommand?.Invoke(command);
+
+ using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
+ var items = new List();
+
+ while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
+ {
+ items.Add(map(reader));
+ }
+
+ return items;
+ }
+
+ public static async Task ExecuteNonQueryAsync(
+ this SqlConnection connection,
+ string commandText,
+ Action configureCommand = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (connection == null)
+ {
+ throw new ArgumentNullException(nameof(connection));
+ }
+
+ using var command = connection.CreateCommand();
+ command.CommandText = commandText;
+ configureCommand?.Invoke(command);
+ return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ public static async Task ExecuteNonQueryAsync(
+ this SqlConnection connection,
+ SqlTransaction transaction,
+ string commandText,
+ Action configureCommand = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (connection == null)
+ {
+ throw new ArgumentNullException(nameof(connection));
+ }
+
+ using var command = new SqlCommand(commandText, connection, transaction);
+ configureCommand?.Invoke(command);
+ return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ public static async Task ExecuteScalarAsync(
+ this SqlConnection connection,
+ string commandText,
+ Action configureCommand = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (connection == null)
+ {
+ throw new ArgumentNullException(nameof(connection));
+ }
+
+ using var command = connection.CreateCommand();
+ command.CommandText = commandText;
+ configureCommand?.Invoke(command);
+
+ var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
+ if (result == null || result is DBNull)
+ {
+ return default!;
+ }
+
+ return (T)Convert.ChangeType(result, typeof(T));
+ }
+
+ public static async Task ExecuteScalarAsync(
+ this SqlConnection connection,
+ SqlTransaction transaction,
+ string commandText,
+ Action configureCommand = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (connection == null)
+ {
+ throw new ArgumentNullException(nameof(connection));
+ }
+
+ using var command = new SqlCommand(commandText, connection, transaction);
+ configureCommand?.Invoke(command);
+
+ var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
+ if (result == null || result is DBNull)
+ {
+ return default!;
+ }
+
+ return (T)Convert.ChangeType(result, typeof(T));
+ }
+
+ public static async Task> QueryOpenConnectionAsync(
+ this IDatabaseConnectionFactory connectionFactory,
+ string commandText,
+ Func map,
+ Action configureCommand = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (connectionFactory == null)
+ {
+ throw new ArgumentNullException(nameof(connectionFactory));
+ }
+
+ await using var connection = await connectionFactory.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
+ return await connection.QueryAsync(commandText, map, configureCommand, cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/XLAB2/Infrastructure/SqlServerConnectionFactory.cs b/XLAB2/Infrastructure/SqlServerConnectionFactory.cs
new file mode 100644
index 0000000..b99b850
--- /dev/null
+++ b/XLAB2/Infrastructure/SqlServerConnectionFactory.cs
@@ -0,0 +1,68 @@
+using Microsoft.Data.SqlClient;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace XLAB2.Infrastructure;
+
+internal sealed class SqlServerConnectionFactory : IDatabaseConnectionFactory
+{
+ private static readonly Lazy CurrentFactory = new(() => new SqlServerConnectionFactory(DatabaseConfiguration.Current), LazyThreadSafetyMode.ExecutionAndPublication);
+
+ private readonly DatabaseOptions _options;
+
+ public SqlServerConnectionFactory(DatabaseOptions options)
+ {
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ ConnectionString = BuildConnectionString(options);
+ }
+
+ public static IDatabaseConnectionFactory Current => CurrentFactory.Value;
+
+ public string ConnectionString { get; }
+
+ public DatabaseOptions Options => _options;
+
+ public SqlConnection CreateConnection()
+ {
+ return new SqlConnection(ConnectionString);
+ }
+
+ public async Task OpenConnectionAsync(CancellationToken cancellationToken = default)
+ {
+ var connection = CreateConnection();
+
+ try
+ {
+ await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
+ return connection;
+ }
+ catch
+ {
+ await connection.DisposeAsync().ConfigureAwait(false);
+ throw;
+ }
+ }
+
+ internal static string BuildConnectionString(DatabaseOptions options)
+ {
+ var builder = new SqlConnectionStringBuilder
+ {
+ ApplicationName = string.IsNullOrWhiteSpace(options.ApplicationName) ? "XLAB2" : options.ApplicationName.Trim(),
+ ConnectRetryCount = Math.Max(0, options.ConnectRetryCount),
+ ConnectRetryInterval = Math.Max(1, options.ConnectRetryIntervalSeconds),
+ ConnectTimeout = Math.Max(1, options.ConnectTimeoutSeconds),
+ DataSource = options.Server.Trim(),
+ Encrypt = options.Encrypt,
+ InitialCatalog = options.Database.Trim(),
+ IntegratedSecurity = options.IntegratedSecurity,
+ MaxPoolSize = Math.Max(1, options.MaxPoolSize),
+ MinPoolSize = Math.Max(0, options.MinPoolSize),
+ MultipleActiveResultSets = options.MultipleActiveResultSets,
+ Pooling = options.Pooling,
+ TrustServerCertificate = options.TrustServerCertificate
+ };
+
+ return builder.ConnectionString;
+ }
+}
diff --git a/XLAB2/MainWindow.xaml b/XLAB2/MainWindow.xaml
new file mode 100644
index 0000000..a7374bd
--- /dev/null
+++ b/XLAB2/MainWindow.xaml
@@ -0,0 +1,472 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XLAB2/MainWindow.xaml.cs b/XLAB2/MainWindow.xaml.cs
new file mode 100644
index 0000000..f52a6af
--- /dev/null
+++ b/XLAB2/MainWindow.xaml.cs
@@ -0,0 +1,101 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace XLAB2
+{
+ public partial class MainWindow : Window
+ {
+ private readonly MainWindowViewModel _viewModel;
+
+ internal MainWindow(PsvDataService service)
+ {
+ InitializeComponent();
+ _viewModel = new MainWindowViewModel(service, new DialogService(this));
+ DataContext = _viewModel;
+ }
+
+ private void DocumentListItem_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ var item = sender as ListBoxItem;
+ if (item != null)
+ {
+ item.IsSelected = true;
+ item.Focus();
+ }
+ }
+
+ private void DocumentLineRow_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ var row = sender as DataGridRow;
+ if (row != null)
+ {
+ row.IsSelected = true;
+ row.Focus();
+ }
+ }
+
+ private void DocumentLineRow_MouseDoubleClick(object sender, MouseButtonEventArgs e)
+ {
+ var row = sender as DataGridRow;
+ if (row == null)
+ {
+ return;
+ }
+
+ row.IsSelected = true;
+ row.Focus();
+ _viewModel.TryEditVerificationFromDoubleClick(row.Item as PsvDocumentLine);
+ }
+
+ private void DocumentGroupRow_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ var row = sender as DataGridRow;
+ if (row != null)
+ {
+ row.IsSelected = true;
+ row.Focus();
+ }
+ }
+
+ private void FrpdDirectoryMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ var window = new FrpdDirectoryWindow();
+ window.Owner = this;
+ window.ShowDialog();
+ }
+
+ private void PrsnDirectoryMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ var window = new PrsnDirectoryWindow();
+ window.Owner = this;
+ window.ShowDialog();
+ }
+
+ private void SpnmtpDirectoryMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ var window = new SpnmtpDirectoryWindow();
+ window.Owner = this;
+ window.ShowDialog();
+ }
+
+ private void TypeSizeDirectoryMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ var window = new TypeSizeDirectoryWindow();
+ window.Owner = this;
+ window.ShowDialog();
+ }
+
+ private void SpoiDirectoryMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ var window = new SpoiDirectoryWindow();
+ window.Owner = this;
+ window.ShowDialog();
+ }
+
+ private async void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ await _viewModel.InitializeAsync();
+ }
+ }
+}
diff --git a/XLAB2/MainWindowViewModel.cs b/XLAB2/MainWindowViewModel.cs
new file mode 100644
index 0000000..d58f8f4
--- /dev/null
+++ b/XLAB2/MainWindowViewModel.cs
@@ -0,0 +1,2499 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Data;
+using System.Windows.Input;
+
+namespace XLAB2
+{
+ internal sealed class MainWindowViewModel : ObservableObject
+ {
+ private readonly List _draftDocuments;
+ private readonly IDialogService _dialogService;
+ private readonly Dictionary> _pendingLinesByDocumentKey;
+ private readonly PsvPrintService _printService;
+ private readonly PsvDataService _service;
+ private string _documentFilterText;
+ private string _documentNumberEditor;
+ private string _documentStatusText;
+ private string _detailTableCountText;
+ private string _groupDetailFilterText;
+ private string _headerDepartmentName;
+ private int _headerInstrumentCount;
+ private DateTime? _headerIssuedOn;
+ private DateTime? _headerReceivedOn;
+ private bool _isBusy;
+ private string _lineStatusText;
+ private PsvDocumentLine _lastCloneSourceLine;
+ private int? _selectedCustomerId;
+ private PsvDocumentSummary _selectedDocument;
+ private PsvDocumentGroupSummary _selectedDocumentGroup;
+ private PsvDocumentLine _selectedDocumentLine;
+ private bool _showClosedDocuments;
+
+ public MainWindowViewModel(PsvDataService service, IDialogService dialogService)
+ {
+ _service = service;
+ _dialogService = dialogService;
+ _printService = new PsvPrintService();
+ _draftDocuments = new List();
+ _pendingLinesByDocumentKey = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ Customers = new ObservableCollection();
+ Documents = new ObservableCollection();
+ DocumentLines = new ObservableCollection();
+ DocumentGroupSummaries = new ObservableCollection();
+
+ DocumentsView = CollectionViewSource.GetDefaultView(Documents);
+ DocumentsView.Filter = FilterDocuments;
+
+ DocumentLinesView = CollectionViewSource.GetDefaultView(DocumentLines);
+ DocumentLinesView.Filter = FilterDocumentLines;
+
+ AddDocumentCommand = new RelayCommand(delegate { AddDocument(); }, delegate { return !IsBusy && !ShowClosedDocuments; });
+ CloneLineVerificationCommand = new RelayCommand(delegate { CloneSelectedLineVerificationAsync(); }, delegate { return CanCloneSelectedLineVerification(); });
+ DeleteDocumentCommand = new RelayCommand(delegate { DeleteDocumentAsync(); }, delegate { return !IsBusy && SelectedDocument != null; });
+ DeleteSelectedLinesCommand = new RelayCommand(delegate { DeleteSelectedLinesAsync(); }, delegate { return CanDeleteSelectedLines(); });
+ DeleteSelectedGroupsCommand = new RelayCommand(delegate { DeleteSelectedGroupsAsync(); }, delegate { return CanDeleteSelectedGroups(); });
+ MarkLinePassedCommand = new RelayCommand(delegate { EditLineVerificationAsync(true); }, delegate { return CanEditSelectedLineVerification(); });
+ MarkLineRejectedCommand = new RelayCommand(delegate { EditLineVerificationAsync(false); }, delegate { return CanEditSelectedLineVerification(); });
+ OpenInstrumentPickerCommand = new RelayCommand(delegate { OpenInstrumentPickerAsync(); }, delegate { return CanAddInstrumentsToSelectedDocument(); });
+ OpenInstrumentTypePickerCommand = new RelayCommand(delegate { OpenInstrumentTypePickerAsync(); }, delegate { return CanAddInstrumentsToSelectedDocument(); });
+ PrintDocumentCommand = new RelayCommand(delegate { PrintSelectedDocumentAsync(); }, delegate { return CanPrintSelectedDocument(); });
+ PrintVerificationDocumentCommand = new RelayCommand(delegate { PrintSelectedVerificationDocumentAsync(); }, delegate { return CanPrintSelectedVerificationDocument(); });
+ RefreshDocumentsCommand = new RelayCommand(delegate { RefreshDocumentsAsync(null, null); }, delegate { return !IsBusy; });
+ ResetLineVerificationCommand = new RelayCommand(delegate { ResetSelectedLineVerificationAsync(); }, delegate { return CanResetSelectedLineVerification(); });
+ SaveDocumentHeaderCommand = new RelayCommand(delegate { SaveDocumentAsync(); }, delegate { return CanSaveDocument(); });
+
+ DocumentStatusText = "Готово.";
+ DetailTableCountText = "Приборов в таблице: 0.";
+ LineStatusText = "Документ не выбран.";
+ }
+
+ public ICommand AddDocumentCommand { get; private set; }
+
+ public ICommand CloneLineVerificationCommand { get; private set; }
+
+ public ObservableCollection Customers { get; private set; }
+
+ public string DocumentFilterText
+ {
+ get { return _documentFilterText; }
+ set
+ {
+ if (SetProperty(ref _documentFilterText, value))
+ {
+ DocumentsView.Refresh();
+ }
+ }
+ }
+
+ public bool ShowClosedDocuments
+ {
+ get { return _showClosedDocuments; }
+ set
+ {
+ if (!SetProperty(ref _showClosedDocuments, value))
+ {
+ return;
+ }
+
+ OnPropertyChanged("ShowOpenDocuments");
+ OnPropertyChanged("IsDocumentHeaderEditable");
+ RaiseCommandStates();
+ RefreshDocumentsAsync(null, null);
+ }
+ }
+
+ public bool ShowOpenDocuments
+ {
+ get { return !ShowClosedDocuments; }
+ set
+ {
+ if (value)
+ {
+ ShowClosedDocuments = false;
+ }
+ }
+ }
+
+ public ObservableCollection DocumentLines { get; private set; }
+
+ public ICollectionView DocumentLinesView { get; private set; }
+
+ public string DocumentNumberEditor
+ {
+ get { return _documentNumberEditor; }
+ set { SetProperty(ref _documentNumberEditor, value); }
+ }
+
+ public ObservableCollection DocumentGroupSummaries { get; private set; }
+
+ public string DocumentStatusText
+ {
+ get { return _documentStatusText; }
+ private set { SetProperty(ref _documentStatusText, value); }
+ }
+
+ public string DetailTableCountText
+ {
+ get { return _detailTableCountText; }
+ private set { SetProperty(ref _detailTableCountText, value); }
+ }
+
+ public ObservableCollection Documents { get; private set; }
+
+ public ICollectionView DocumentsView { get; private set; }
+
+ public ICommand DeleteDocumentCommand { get; private set; }
+
+ public ICommand DeleteSelectedLinesCommand { get; private set; }
+
+ public ICommand DeleteSelectedGroupsCommand { get; private set; }
+
+ public string GroupDetailFilterText
+ {
+ get { return _groupDetailFilterText; }
+ set
+ {
+ if (SetProperty(ref _groupDetailFilterText, value))
+ {
+ RefreshDocumentLinesView();
+ }
+ }
+ }
+
+ public string HeaderDepartmentName
+ {
+ get { return _headerDepartmentName; }
+ private set { SetProperty(ref _headerDepartmentName, value); }
+ }
+
+ public int HeaderInstrumentCount
+ {
+ get { return _headerInstrumentCount; }
+ private set { SetProperty(ref _headerInstrumentCount, value); }
+ }
+
+ public bool IsDocumentHeaderEditable
+ {
+ get
+ {
+ return !IsBusy
+ && SelectedDocument != null
+ && !IsDocumentClosed(SelectedDocument);
+ }
+ }
+
+ public DateTime? HeaderIssuedOn
+ {
+ get { return _headerIssuedOn; }
+ set { SetProperty(ref _headerIssuedOn, value); }
+ }
+
+ public DateTime? HeaderReceivedOn
+ {
+ get { return _headerReceivedOn; }
+ set { SetProperty(ref _headerReceivedOn, value); }
+ }
+
+ public bool IsBusy
+ {
+ get { return _isBusy; }
+ private set
+ {
+ if (SetProperty(ref _isBusy, value))
+ {
+ RaiseCommandStates();
+ OnPropertyChanged("IsCustomerEditable");
+ OnPropertyChanged("IsDocumentHeaderEditable");
+ }
+ }
+ }
+
+ public bool IsCustomerEditable
+ {
+ get
+ {
+ return !IsBusy
+ && SelectedDocument != null
+ && SelectedDocument.IsDraft
+ && GetPendingLines(SelectedDocument).Count == 0;
+ }
+ }
+
+ public string LineStatusText
+ {
+ get { return _lineStatusText; }
+ private set { SetProperty(ref _lineStatusText, value); }
+ }
+
+ public ICommand MarkLinePassedCommand { get; private set; }
+
+ public ICommand MarkLineRejectedCommand { get; private set; }
+
+ public ICommand OpenInstrumentPickerCommand { get; private set; }
+
+ public ICommand OpenInstrumentTypePickerCommand { get; private set; }
+
+ public ICommand PrintDocumentCommand { get; private set; }
+
+ public ICommand PrintVerificationDocumentCommand { get; private set; }
+
+ public ICommand RefreshDocumentsCommand { get; private set; }
+
+ public ICommand ResetLineVerificationCommand { get; private set; }
+
+ public ICommand SaveDocumentHeaderCommand { get; private set; }
+
+ public int? SelectedCustomerId
+ {
+ get { return _selectedCustomerId; }
+ set
+ {
+ if (!SetProperty(ref _selectedCustomerId, value))
+ {
+ return;
+ }
+
+ ApplySelectedCustomer();
+ }
+ }
+
+ public PsvDocumentSummary SelectedDocument
+ {
+ get { return _selectedDocument; }
+ set
+ {
+ if (SetProperty(ref _selectedDocument, value))
+ {
+ _lastCloneSourceLine = null;
+ FillHeaderFromSelection();
+ RaiseCommandStates();
+ OnPropertyChanged("IsCustomerEditable");
+ OnPropertyChanged("IsDocumentHeaderEditable");
+ LoadSelectedDocumentAsync();
+ }
+ }
+ }
+
+ public PsvDocumentGroupSummary SelectedDocumentGroup
+ {
+ get { return _selectedDocumentGroup; }
+ set
+ {
+ if (SetProperty(ref _selectedDocumentGroup, value))
+ {
+ RefreshDocumentLinesView();
+ }
+ }
+ }
+
+ public PsvDocumentLine SelectedDocumentLine
+ {
+ get { return _selectedDocumentLine; }
+ set
+ {
+ if (SetProperty(ref _selectedDocumentLine, value))
+ {
+ if (CanUseLineAsCloneSource(value))
+ {
+ _lastCloneSourceLine = value;
+ }
+
+ RaiseCommandStates();
+ }
+ }
+ }
+
+ public async Task InitializeAsync()
+ {
+ await ExecuteBusyOperationAsync(async delegate
+ {
+ await LoadCustomersCoreAsync();
+ await RefreshDocumentsCoreAsync(null, null);
+ });
+ }
+
+ private void AddDocument()
+ {
+ var request = _dialogService.ShowCreateDocumentDialog(new DocumentEditorResult
+ {
+ AcceptedOn = DateTime.Today,
+ IssuedOn = null,
+ DocumentNumber = string.Empty
+ });
+
+ if (request == null)
+ {
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(request.DocumentNumber))
+ {
+ _dialogService.ShowWarning("Введите номер ПСВ.");
+ return;
+ }
+
+ if (DocumentExistsInCollections(request.DocumentNumber, null))
+ {
+ _dialogService.ShowWarning("ПСВ с таким номером уже есть в списке.");
+ return;
+ }
+
+ var draft = new PsvDocumentSummary
+ {
+ DocumentKey = Guid.NewGuid().ToString("N"),
+ DocumentNumber = request.DocumentNumber.Trim(),
+ AcceptedOn = request.AcceptedOn,
+ IssuedOn = null,
+ CustomerName = string.Empty,
+ CustomerId = null,
+ DepartmentName = string.Empty,
+ ItemCount = 0,
+ IssuedCount = 0,
+ PassedCount = 0,
+ FailedCount = 0,
+ IsDraft = true
+ };
+
+ _draftDocuments.Add(draft);
+ InsertDraftIntoCollection(draft);
+ DocumentsView.Refresh();
+ SelectedDocument = draft;
+ DocumentStatusText = BuildDocumentStatusText(Documents.Count);
+ }
+
+ private void ApplySelectedCustomer()
+ {
+ if (SelectedDocument == null || !SelectedDocument.IsDraft)
+ {
+ return;
+ }
+
+ SelectedDocument.CustomerId = SelectedCustomerId;
+ var customer = Customers.FirstOrDefault(delegate(CustomerReference item) { return item.CustomerId == SelectedCustomerId; });
+ SelectedDocument.CustomerName = customer == null ? string.Empty : customer.CustomerName;
+ DocumentsView.Refresh();
+ RaiseCommandStates();
+ }
+
+ private bool CanSaveDocument()
+ {
+ if (IsBusy || SelectedDocument == null)
+ {
+ return false;
+ }
+
+ if (IsDocumentClosed(SelectedDocument))
+ {
+ return false;
+ }
+
+ if (!SelectedDocument.IsDraft)
+ {
+ return true;
+ }
+
+ return GetPendingLines(SelectedDocument).Count > 0;
+ }
+
+ private bool CanDeleteSelectedGroups()
+ {
+ return CanModifySelectedDocument()
+ && GetDeleteTargetGroups().Count > 0;
+ }
+
+ private bool CanDeleteSelectedLines()
+ {
+ return CanModifySelectedDocument()
+ && GetDeleteTargetLines().Count > 0;
+ }
+
+ private bool CanPrintSelectedDocument()
+ {
+ return !IsBusy
+ && SelectedDocument != null
+ && !SelectedDocument.IsDraft;
+ }
+
+ private bool CanEditSelectedLineVerification()
+ {
+ var targetLines = GetVerificationTargetLines();
+ return CanModifySelectedDocument()
+ && targetLines.Count > 0
+ && targetLines.All(delegate(PsvDocumentLine line) { return !HasVerificationData(line); });
+ }
+
+ private bool CanCloneSelectedLineVerification()
+ {
+ var sourceLine = ResolveCloneSourceLine();
+ return CanModifySelectedDocument()
+ && CanUseLineAsCloneSource(sourceLine)
+ && GetCheckedCloneTargetLines(sourceLine).Count > 0;
+ }
+
+ private bool CanPrintSelectedVerificationDocument()
+ {
+ return !IsBusy && HasPrintableVerificationDocument(SelectedDocumentLine);
+ }
+
+ private bool CanResetSelectedLineVerification()
+ {
+ var targetLines = GetVerificationTargetLines();
+ return CanModifySelectedDocument()
+ && targetLines.Count > 0
+ && targetLines.All(HasVerificationData);
+ }
+
+ private bool CanModifySelectedDocument()
+ {
+ return !IsBusy
+ && SelectedDocument != null
+ && !IsDocumentClosed(SelectedDocument);
+ }
+
+ private bool CanAddInstrumentsToSelectedDocument()
+ {
+ return CanModifySelectedDocument() && SelectedDocument.CustomerId.HasValue;
+ }
+
+ private static bool IsDocumentClosed(PsvDocumentSummary document)
+ {
+ return document != null && document.IssuedOn.HasValue;
+ }
+
+ private static bool HasVerificationData(PsvDocumentLine line)
+ {
+ return line != null
+ && (line.IsPassed.HasValue
+ || line.VerificationPerformedOn.HasValue
+ || line.VerifierId.HasValue
+ || !string.IsNullOrWhiteSpace(line.StickerNumber)
+ || line.VerificationDocumentFormId.HasValue
+ || line.VerificationDocumentLinkTypeId.HasValue
+ || !string.IsNullOrWhiteSpace(line.VerificationDocumentNumber)
+ || line.VerificationDocumentDate.HasValue
+ || !string.IsNullOrWhiteSpace(line.RejectionReason));
+ }
+
+ private static bool CanUseLineAsCloneSource(PsvDocumentLine line)
+ {
+ if (line == null || !line.IsPassed.HasValue || !line.VerifierId.HasValue)
+ {
+ return false;
+ }
+
+ if (!line.VerificationPerformedOn.HasValue && !line.VerificationDocumentDate.HasValue)
+ {
+ return false;
+ }
+
+ if (!line.IsPassed.Value
+ && (string.IsNullOrWhiteSpace(line.RejectionReason)
+ || string.IsNullOrWhiteSpace(line.VerificationDocumentNumber)))
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(line.VerificationDocumentNumber)
+ && (!line.VerificationDocumentFormId.HasValue || !line.VerificationDocumentLinkTypeId.HasValue))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static bool HasPrintableVerificationDocument(PsvDocumentLine line)
+ {
+ if (line == null || !line.IsPassed.HasValue)
+ {
+ return false;
+ }
+
+ if (string.IsNullOrWhiteSpace(line.VerificationDocumentNumber))
+ {
+ return false;
+ }
+
+ if (!line.VerificationPerformedOn.HasValue && !line.VerificationDocumentDate.HasValue)
+ {
+ return false;
+ }
+
+ if (!line.IsPassed.Value && string.IsNullOrWhiteSpace(line.RejectionReason))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool CanEditVerificationFromDoubleClick(PsvDocumentLine line)
+ {
+ return CanModifySelectedDocument()
+ && line != null
+ && line.IsPassed.HasValue
+ && HasVerificationData(line);
+ }
+
+ private List GetCheckedDocumentLines()
+ {
+ return DocumentLinesView.Cast