This commit is contained in:
Курнат Андрей
2026-04-03 21:06:10 +03:00
parent 1b20b39aca
commit a2b4762702
16 changed files with 1170 additions and 33 deletions

View File

@@ -0,0 +1,118 @@
<Window x:Class="XLAB2.AccountingBookDirectoryWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Книги учета"
Height="680"
Width="980"
MinHeight="520"
MinWidth="820"
Loaded="Window_Loaded"
WindowStartupLocation="CenterOwner">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0"
Margin="0,0,0,8">
<StackPanel DockPanel.Dock="Left"
Orientation="Horizontal">
<TextBlock Width="210"
VerticalAlignment="Center"
Text="Поиск по ключу или книге учета" />
<TextBox Width="320"
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel DockPanel.Dock="Right"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Width="110"
Margin="8,0,0,0"
Command="{Binding RefreshCommand}"
Content="Обновить" />
</StackPanel>
</DockPanel>
<DataGrid Grid.Row="1"
ItemsSource="{Binding ItemsView}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
AutoGenerateColumns="False"
CanUserAddRows="False"
IsReadOnly="True"
HeadersVisibility="Column">
<DataGrid.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Добавить"
Command="{Binding AddCommand}">
<MenuItem.Icon>
<Viewbox Width="14" Height="14">
<Grid Width="16" Height="16">
<Ellipse Width="14" Height="14" Fill="{StaticResource AppMenuIconAccentBrush}" />
<Rectangle Width="2" Height="8" Fill="White" RadiusX="1" RadiusY="1" />
<Rectangle Width="8" Height="2" Fill="White" RadiusX="1" RadiusY="1" />
</Grid>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Изменить"
Command="{Binding EditCommand}">
<MenuItem.Icon>
<Viewbox Width="14" Height="14">
<Canvas Width="16" Height="16">
<Path Fill="{StaticResource AppMenuIconAccentBrush}" Data="M11.7,1.4 L14.6,4.3 L5.5,13.4 L2.5,13.9 L3,10.9 Z" />
<Path Fill="#FFEAF3FB" Data="M10.7,2.4 L13.6,5.3 L12.8,6.1 L9.9,3.2 Z" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Удалить"
Command="{Binding DeleteCommand}">
<MenuItem.Icon>
<Viewbox Width="14" Height="14">
<Canvas Width="16" Height="16">
<Rectangle Canvas.Left="4" Canvas.Top="5" Width="8" Height="8" RadiusX="1" RadiusY="1" Fill="{StaticResource AppMenuIconDangerBrush}" />
<Rectangle Canvas.Left="3" Canvas.Top="3" Width="10" Height="2" RadiusX="1" RadiusY="1" Fill="{StaticResource AppMenuIconDangerBrush}" />
<Rectangle Canvas.Left="6" Canvas.Top="1.5" Width="4" Height="2" RadiusX="1" RadiusY="1" Fill="{StaticResource AppMenuIconDangerBrush}" />
<Rectangle Canvas.Left="6" Canvas.Top="6.5" Width="1" Height="5" Fill="White" />
<Rectangle Canvas.Left="9" Canvas.Top="6.5" Width="1" Height="5" Fill="White" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<EventSetter Event="PreviewMouseRightButtonDown"
Handler="DataGridRow_PreviewMouseRightButtonDown" />
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Ключ"
Width="240"
Binding="{Binding Key}" />
<DataGridTextColumn Header="Номер книги учета"
Width="*"
Binding="{Binding Title}" />
</DataGrid.Columns>
</DataGrid>
<TextBlock Grid.Row="2"
Margin="0,8,0,0"
Foreground="DimGray"
Text="{Binding StatusText}" />
<StackPanel Grid.Row="3"
Margin="0,12,0,0"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Width="90"
IsCancel="True"
Content="Закрыть" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,35 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace XLAB2
{
public partial class AccountingBookDirectoryWindow : Window
{
private readonly AccountingBookDirectoryWindowViewModel _viewModel;
internal AccountingBookDirectoryWindow(DocumentNumberDirectoryService documentNumberDirectoryService)
{
InitializeComponent();
_viewModel = new AccountingBookDirectoryWindowViewModel(
documentNumberDirectoryService,
new DialogService(this, documentNumberDirectoryService));
DataContext = _viewModel;
}
private void DataGridRow_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
var row = sender as DataGridRow;
if (row != null)
{
row.IsSelected = true;
row.Focus();
}
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
_viewModel.Initialize();
}
}
}

View File

@@ -0,0 +1,266 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows.Data;
using System.Windows.Input;
namespace XLAB2
{
internal sealed class AccountingBookDirectoryWindowViewModel : ObservableObject
{
private readonly IDialogService _dialogService;
private readonly DocumentNumberDirectoryService _service;
private string _searchText;
private GroupOption _selectedItem;
private string _statusText;
public AccountingBookDirectoryWindowViewModel(DocumentNumberDirectoryService service, IDialogService dialogService)
{
_service = service ?? throw new ArgumentNullException("service");
_dialogService = dialogService ?? throw new ArgumentNullException("dialogService");
Items = new ObservableCollection<GroupOption>();
ItemsView = CollectionViewSource.GetDefaultView(Items);
ItemsView.Filter = FilterItems;
AddCommand = new RelayCommand(delegate { AddItem(); });
DeleteCommand = new RelayCommand(delegate { DeleteSelectedItem(); }, delegate { return SelectedItem != null; });
EditCommand = new RelayCommand(delegate { EditSelectedItem(); }, delegate { return SelectedItem != null; });
RefreshCommand = new RelayCommand(delegate { RefreshItems(null); });
UpdateStatus();
}
public ICommand AddCommand { get; private set; }
public ICommand DeleteCommand { get; private set; }
public ICommand EditCommand { get; private set; }
public ObservableCollection<GroupOption> Items { get; private set; }
public ICollectionView ItemsView { get; private set; }
public ICommand RefreshCommand { get; private set; }
public string SearchText
{
get { return _searchText; }
set
{
if (SetProperty(ref _searchText, value))
{
ItemsView.Refresh();
UpdateStatus();
}
}
}
public GroupOption SelectedItem
{
get { return _selectedItem; }
set
{
if (SetProperty(ref _selectedItem, value))
{
RaiseCommandStates();
}
}
}
public string StatusText
{
get { return _statusText; }
private set { SetProperty(ref _statusText, value); }
}
public void Initialize()
{
RunOperation(delegate { RefreshItems(null); }, false);
}
private void AddItem()
{
var result = _dialogService.ShowAccountingBookEditDialog(new GroupOption(), true, Items.ToList());
if (result == null)
{
return;
}
RunOperation(delegate
{
var items = CloneItems(Items);
items.Add(CloneItem(result));
_service.SaveAccountingBooks(items);
RefreshItems(result.Key);
_dialogService.ShowInfo("Запись справочника добавлена.");
}, true);
}
private bool Contains(string source, string searchText)
{
return !string.IsNullOrWhiteSpace(source)
&& source.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0;
}
private void DeleteSelectedItem()
{
if (SelectedItem == null)
{
return;
}
var selectedItem = SelectedItem;
if (!_dialogService.Confirm(string.Format("Удалить книгу учета \"{0}\"?", selectedItem.Title)))
{
return;
}
RunOperation(delegate
{
var items = CloneItems(Items);
items.RemoveAll(delegate(GroupOption item)
{
return string.Equals(item.Key, selectedItem.Key, StringComparison.OrdinalIgnoreCase);
});
_service.SaveAccountingBooks(items);
RefreshItems(null);
_dialogService.ShowInfo("Запись справочника удалена.");
}, true);
}
private void EditSelectedItem()
{
if (SelectedItem == null)
{
return;
}
var seed = CloneItem(SelectedItem);
var result = _dialogService.ShowAccountingBookEditDialog(seed, false, Items.ToList());
if (result == null)
{
return;
}
RunOperation(delegate
{
var items = CloneItems(Items);
var index = items.FindIndex(delegate(GroupOption item)
{
return string.Equals(item.Key, seed.Key, StringComparison.OrdinalIgnoreCase);
});
if (index < 0)
{
throw new InvalidOperationException("Не удалось найти книгу учета для обновления.");
}
items[index] = CloneItem(result);
_service.SaveAccountingBooks(items);
RefreshItems(result.Key);
_dialogService.ShowInfo("Запись справочника обновлена.");
}, true);
}
private bool FilterItems(object item)
{
var directoryItem = item as GroupOption;
if (directoryItem == null)
{
return false;
}
if (string.IsNullOrWhiteSpace(SearchText))
{
return true;
}
return Contains(directoryItem.Key, SearchText)
|| Contains(directoryItem.Title, SearchText);
}
private void RaiseCommandStates()
{
((RelayCommand)AddCommand).RaiseCanExecuteChanged();
((RelayCommand)DeleteCommand).RaiseCanExecuteChanged();
((RelayCommand)EditCommand).RaiseCanExecuteChanged();
((RelayCommand)RefreshCommand).RaiseCanExecuteChanged();
}
private void RefreshItems(string keyToSelect)
{
var items = _service.LoadAccountingBooks();
var currentKey = string.IsNullOrWhiteSpace(keyToSelect)
? (SelectedItem == null ? null : SelectedItem.Key)
: keyToSelect;
Items.Clear();
foreach (var item in items)
{
Items.Add(CloneItem(item));
}
ItemsView.Refresh();
SelectedItem = string.IsNullOrWhiteSpace(currentKey)
? Items.FirstOrDefault()
: Items.FirstOrDefault(delegate(GroupOption item)
{
return string.Equals(item.Key, currentKey, StringComparison.OrdinalIgnoreCase);
}) ?? Items.FirstOrDefault();
UpdateStatus();
}
private void RunOperation(Action action, bool showWarningForInvalidOperation)
{
try
{
action();
}
catch (InvalidOperationException ex)
{
if (showWarningForInvalidOperation)
{
_dialogService.ShowWarning(ex.Message);
return;
}
_dialogService.ShowError(ex.Message);
}
catch (Exception ex)
{
_dialogService.ShowError(ex.Message);
}
}
private static GroupOption CloneItem(GroupOption item)
{
return new GroupOption
{
Key = item == null ? string.Empty : item.Key,
Title = item == null ? string.Empty : item.Title
};
}
private static List<GroupOption> CloneItems(IEnumerable<GroupOption> items)
{
var result = new List<GroupOption>();
foreach (var item in items ?? Array.Empty<GroupOption>())
{
result.Add(CloneItem(item));
}
return result;
}
private void UpdateStatus()
{
var visibleCount = ItemsView.Cast<object>().Count();
StatusText = string.Format("Всего записей: {0}. По фильтру: {1}.", Items.Count, visibleCount);
}
}
}

View File

@@ -0,0 +1,64 @@
<Window x:Class="XLAB2.AccountingBookEditWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Binding Title}"
Height="250"
Width="560"
MinHeight="240"
MinWidth="520"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0"
Margin="0,0,12,8"
VerticalAlignment="Center"
Text="Номер книги учета" />
<TextBox Grid.Row="0"
Grid.Column="1"
Margin="0,0,0,8"
Text="{Binding BookNumber, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="1"
Grid.Column="0"
Margin="0,0,12,8"
VerticalAlignment="Center"
Text="Ключ" />
<TextBox Grid.Row="1"
Grid.Column="1"
Margin="0,0,0,8"
Text="{Binding Key, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="2"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
Foreground="Firebrick"
Text="{Binding ValidationMessage}" />
<StackPanel Grid.Row="3"
Grid.ColumnSpan="2"
Margin="0,12,0,0"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Width="100"
Margin="0,0,8,0"
IsDefault="True"
Command="{Binding ConfirmCommand}"
Content="Сохранить" />
<Button Width="90"
Command="{Binding CancelCommand}"
Content="Отмена" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,20 @@
using System.Windows;
namespace XLAB2
{
public partial class AccountingBookEditWindow : Window
{
internal AccountingBookEditWindow(AccountingBookEditWindowViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
viewModel.CloseRequested += ViewModelOnCloseRequested;
}
private void ViewModelOnCloseRequested(object sender, bool? dialogResult)
{
DialogResult = dialogResult;
Close();
}
}
}

View File

@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
namespace XLAB2
{
internal sealed class AccountingBookEditWindowViewModel : ObservableObject
{
private readonly IReadOnlyList<GroupOption> _existingItems;
private readonly string _originalKey;
private string _bookNumber;
private string _key;
private string _validationMessage;
public AccountingBookEditWindowViewModel(GroupOption seed, bool isNew, IReadOnlyList<GroupOption> existingItems)
{
var source = seed ?? new GroupOption();
_existingItems = existingItems ?? Array.Empty<GroupOption>();
_originalKey = Normalize(source.Key);
IsNew = isNew;
BookNumber = source.Title ?? string.Empty;
Key = source.Key ?? string.Empty;
ConfirmCommand = new RelayCommand(Confirm);
CancelCommand = new RelayCommand(Cancel);
}
public event EventHandler<bool?> CloseRequested;
public string BookNumber
{
get { return _bookNumber; }
set { SetProperty(ref _bookNumber, value); }
}
public ICommand CancelCommand { get; private set; }
public ICommand ConfirmCommand { get; private set; }
public bool IsNew { get; private set; }
public string Key
{
get { return _key; }
set { SetProperty(ref _key, value); }
}
public string Title
{
get { return IsNew ? "Новая книга учета" : "Редактирование книги учета"; }
}
public string ValidationMessage
{
get { return _validationMessage; }
private set { SetProperty(ref _validationMessage, value); }
}
public GroupOption ToResult()
{
return new GroupOption
{
Key = Normalize(Key) ?? string.Empty,
Title = Normalize(BookNumber) ?? string.Empty
};
}
private void Cancel(object parameter)
{
RaiseCloseRequested(false);
}
private void Confirm(object parameter)
{
var normalizedKey = Normalize(Key);
if (normalizedKey == null)
{
ValidationMessage = "Укажите ключ книги учета.";
return;
}
var normalizedBookNumber = Normalize(BookNumber);
if (normalizedBookNumber == null)
{
ValidationMessage = "Укажите номер книги учета.";
return;
}
var duplicateKey = _existingItems.FirstOrDefault(delegate(GroupOption item)
{
return item != null
&& !IsCurrentItem(item)
&& string.Equals(Normalize(item.Key), normalizedKey, StringComparison.OrdinalIgnoreCase);
});
if (duplicateKey != null)
{
ValidationMessage = string.Format("Ключ книги учета \"{0}\" уже существует.", normalizedKey);
return;
}
var duplicateBookNumber = _existingItems.FirstOrDefault(delegate(GroupOption item)
{
return item != null
&& !IsCurrentItem(item)
&& string.Equals(Normalize(item.Title), normalizedBookNumber, StringComparison.OrdinalIgnoreCase);
});
if (duplicateBookNumber != null)
{
ValidationMessage = string.Format("Книга учета \"{0}\" уже существует.", normalizedBookNumber);
return;
}
ValidationMessage = string.Empty;
RaiseCloseRequested(true);
}
private bool IsCurrentItem(GroupOption item)
{
return !IsNew
&& string.Equals(Normalize(item == null ? null : item.Key), _originalKey, StringComparison.OrdinalIgnoreCase);
}
private static string Normalize(string value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
private void RaiseCloseRequested(bool? dialogResult)
{
var handler = CloseRequested;
if (handler != null)
{
handler(this, dialogResult);
}
}
}
}

View File

@@ -22,8 +22,11 @@ namespace XLAB2
.ConfigureServices((_, services) => .ConfigureServices((_, services) =>
{ {
services.AddSingleton<IDatabaseConnectionFactory>(_ => SqlServerConnectionFactory.Current); services.AddSingleton<IDatabaseConnectionFactory>(_ => SqlServerConnectionFactory.Current);
services.AddSingleton(new DocumentNumberDirectoryService(System.IO.Path.Combine(AppContext.BaseDirectory, "Assets", "document-number-directory.json")));
services.AddTransient<PsvDataService>(); services.AddTransient<PsvDataService>();
services.AddTransient<MainWindow>(provider => new MainWindow(provider.GetRequiredService<PsvDataService>())); services.AddTransient<MainWindow>(provider => new MainWindow(
provider.GetRequiredService<PsvDataService>(),
provider.GetRequiredService<DocumentNumberDirectoryService>()));
}) })
.UseDefaultServiceProvider((_, options) => .UseDefaultServiceProvider((_, options) =>
{ {

View File

@@ -0,0 +1,18 @@
{
"documentTypes": [
{
"key": "PSV",
"title": "ПСВ"
},
{
"key": "ACT_REFERENCE",
"title": "Акт-справка"
}
],
"accountingBooks": [
{
"key": "219/С5/15",
"title": "219/С5/15"
}
]
}

View File

@@ -1,9 +1,9 @@
<Window x:Class="XLAB2.CreateDocumentWindow" <Window x:Class="XLAB2.CreateDocumentWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Новый ПСВ" Title="Новый документ"
Height="240" Height="360"
Width="430" Width="520"
ResizeMode="NoResize" ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"> WindowStartupLocation="CenterOwner">
<Grid Margin="16"> <Grid Margin="16">
@@ -13,9 +13,12 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="150" /> <ColumnDefinition Width="170" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
@@ -23,36 +26,72 @@
Grid.Column="0" Grid.Column="0"
Margin="0,0,12,8" Margin="0,0,12,8"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="Номер ПСВ" /> Text="Тип документа" />
<TextBox Grid.Row="0" <ComboBox Grid.Row="0"
Grid.Column="1" Grid.Column="1"
MinWidth="180"
Margin="0,0,0,8" Margin="0,0,0,8"
Text="{Binding DocumentNumber, UpdateSourceTrigger=PropertyChanged}" /> DisplayMemberPath="Title"
ItemsSource="{Binding DocumentTypes}"
SelectedItem="{Binding SelectedDocumentType, Mode=TwoWay}" />
<TextBlock Grid.Row="1" <TextBlock Grid.Row="1"
Grid.Column="0" Grid.Column="0"
Margin="0,0,12,8" Margin="0,0,12,8"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="Дата приемки" /> Text="Книга учета" />
<DatePicker Grid.Row="1" <ComboBox Grid.Row="1"
Grid.Column="1" Grid.Column="1"
SelectedDate="{Binding AcceptedOn, Mode=TwoWay}" Margin="0,0,0,8"
SelectedDateFormat="Short" DisplayMemberPath="Title"
Margin="0,0,0,8" /> ItemsSource="{Binding AccountingBooks}"
SelectedItem="{Binding SelectedAccountingBook, Mode=TwoWay}" />
<TextBlock Grid.Row="2" <TextBlock Grid.Row="2"
Grid.Column="0"
Margin="0,0,12,8"
VerticalAlignment="Center"
Text="Номер документа" />
<TextBox Grid.Row="2"
Grid.Column="1"
MinWidth="180"
Margin="0,0,0,8"
Text="{Binding DocumentSequenceNumber, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="3"
Grid.Column="0"
Margin="0,0,12,8"
VerticalAlignment="Center"
Text="Итоговый номер" />
<TextBox Grid.Row="3"
Grid.Column="1"
Margin="0,0,0,8"
IsReadOnly="True"
Text="{Binding DocumentNumber, Mode=OneWay}" />
<TextBlock Grid.Row="4"
Grid.Column="0"
Margin="0,0,12,8"
VerticalAlignment="Center"
Text="Дата приемки" />
<DatePicker Grid.Row="4"
Grid.Column="1"
Margin="0,0,0,8"
SelectedDate="{Binding AcceptedOn, Mode=TwoWay}"
SelectedDateFormat="Short" />
<TextBlock Grid.Row="5"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Margin="0,8,0,4" Margin="0,8,0,4"
TextWrapping="Wrap"
Foreground="DimGray" Foreground="DimGray"
Text="ПСВ без строк EKZMK не хранится в базе. Команда «Добавить» создаёт только черновик текущего сеанса." /> TextWrapping="Wrap"
<TextBlock Grid.Row="3" Text="Документ без строк EKZMK не хранится в базе. Команда «Создать» создаёт только черновик текущего сеанса." />
<TextBlock Grid.Row="6"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Foreground="Firebrick" Foreground="Firebrick"
Text="{Binding ValidationMessage}" /> Text="{Binding ValidationMessage}" />
<StackPanel Grid.Row="4" <StackPanel Grid.Row="7"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Orientation="Horizontal" Orientation="Horizontal"
HorizontalAlignment="Right"> HorizontalAlignment="Right">

View File

@@ -1,4 +1,6 @@
using System; using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input; using System.Windows.Input;
namespace XLAB2 namespace XLAB2
@@ -6,13 +8,25 @@ namespace XLAB2
internal sealed class CreateDocumentWindowViewModel : ObservableObject internal sealed class CreateDocumentWindowViewModel : ObservableObject
{ {
private DateTime? _acceptedOn; private DateTime? _acceptedOn;
private string _documentNumber; private string _documentSequenceNumber;
private GroupOption _selectedAccountingBook;
private GroupOption _selectedDocumentType;
private string _validationMessage; private string _validationMessage;
public CreateDocumentWindowViewModel(DocumentEditorResult seed) public CreateDocumentWindowViewModel(DocumentEditorResult seed, DocumentNumberDirectory directory)
{ {
if (directory == null)
{
throw new ArgumentNullException("directory");
}
AccountingBooks = new ObservableCollection<GroupOption>(directory.AccountingBooks ?? Array.Empty<GroupOption>());
DocumentTypes = new ObservableCollection<GroupOption>(directory.DocumentTypes ?? Array.Empty<GroupOption>());
_acceptedOn = seed != null ? seed.AcceptedOn : DateTime.Today; _acceptedOn = seed != null ? seed.AcceptedOn : DateTime.Today;
_documentNumber = seed != null ? seed.DocumentNumber : string.Empty; _documentSequenceNumber = seed == null ? string.Empty : seed.DocumentSequenceNumber ?? string.Empty;
_selectedAccountingBook = ResolveSelectedOption(AccountingBooks, seed == null ? null : seed.AccountingBookKey);
_selectedDocumentType = ResolveSelectedOption(DocumentTypes, seed == null ? null : seed.DocumentTypeKey);
ConfirmCommand = new RelayCommand(Confirm); ConfirmCommand = new RelayCommand(Confirm);
CancelCommand = new RelayCommand(Cancel); CancelCommand = new RelayCommand(Cancel);
@@ -23,8 +37,16 @@ namespace XLAB2
public DateTime? AcceptedOn public DateTime? AcceptedOn
{ {
get { return _acceptedOn; } get { return _acceptedOn; }
set { SetProperty(ref _acceptedOn, value); } set
{
if (SetProperty(ref _acceptedOn, value))
{
ClearValidationMessage();
} }
}
}
public ObservableCollection<GroupOption> AccountingBooks { get; private set; }
public ICommand CancelCommand { get; private set; } public ICommand CancelCommand { get; private set; }
@@ -32,8 +54,54 @@ namespace XLAB2
public string DocumentNumber public string DocumentNumber
{ {
get { return _documentNumber; } get
set { SetProperty(ref _documentNumber, value); } {
return DocumentNumberFormatter.Build(
SelectedDocumentType == null ? null : SelectedDocumentType.Title,
SelectedAccountingBook == null ? null : SelectedAccountingBook.Title,
DocumentSequenceNumber);
}
}
public string DocumentSequenceNumber
{
get { return _documentSequenceNumber; }
set
{
if (SetProperty(ref _documentSequenceNumber, value))
{
ClearValidationMessage();
OnPropertyChanged("DocumentNumber");
}
}
}
public ObservableCollection<GroupOption> DocumentTypes { get; private set; }
public GroupOption SelectedAccountingBook
{
get { return _selectedAccountingBook; }
set
{
if (SetProperty(ref _selectedAccountingBook, value))
{
ClearValidationMessage();
OnPropertyChanged("DocumentNumber");
}
}
}
public GroupOption SelectedDocumentType
{
get { return _selectedDocumentType; }
set
{
if (SetProperty(ref _selectedDocumentType, value))
{
ClearValidationMessage();
OnPropertyChanged("DocumentNumber");
}
}
} }
public string ValidationMessage public string ValidationMessage
@@ -46,7 +114,12 @@ namespace XLAB2
{ {
return new DocumentEditorResult return new DocumentEditorResult
{ {
DocumentNumber = DocumentNumber == null ? string.Empty : DocumentNumber.Trim(), AccountingBookKey = SelectedAccountingBook == null ? string.Empty : SelectedAccountingBook.Key,
AccountingBookTitle = SelectedAccountingBook == null ? string.Empty : SelectedAccountingBook.Title,
DocumentNumber = DocumentNumber,
DocumentSequenceNumber = DocumentSequenceNumber == null ? string.Empty : DocumentSequenceNumber.Trim(),
DocumentTypeKey = SelectedDocumentType == null ? string.Empty : SelectedDocumentType.Key,
DocumentTypeTitle = SelectedDocumentType == null ? string.Empty : SelectedDocumentType.Title,
AcceptedOn = AcceptedOn.HasValue ? AcceptedOn.Value : DateTime.Today, AcceptedOn = AcceptedOn.HasValue ? AcceptedOn.Value : DateTime.Today,
IssuedOn = null IssuedOn = null
}; };
@@ -57,11 +130,31 @@ namespace XLAB2
RaiseCloseRequested(false); RaiseCloseRequested(false);
} }
private void ClearValidationMessage()
{
if (!string.IsNullOrEmpty(ValidationMessage))
{
ValidationMessage = string.Empty;
}
}
private void Confirm(object parameter) private void Confirm(object parameter)
{ {
if (string.IsNullOrWhiteSpace(DocumentNumber)) if (SelectedDocumentType == null)
{ {
ValidationMessage = "Введите номер ПСВ."; ValidationMessage = "Выберите тип документа.";
return;
}
if (SelectedAccountingBook == null)
{
ValidationMessage = "Выберите книгу учета.";
return;
}
if (string.IsNullOrWhiteSpace(DocumentSequenceNumber))
{
ValidationMessage = "Введите номер документа.";
return; return;
} }
@@ -71,10 +164,42 @@ namespace XLAB2
return; return;
} }
if (DocumentNumber.Length > DocumentNumberFormatter.MaxLength)
{
ValidationMessage = string.Format(
"Итоговый номер документа не должен превышать {0} символов.",
DocumentNumberFormatter.MaxLength);
return;
}
ValidationMessage = string.Empty; ValidationMessage = string.Empty;
RaiseCloseRequested(true); RaiseCloseRequested(true);
} }
private static GroupOption ResolveSelectedOption(ObservableCollection<GroupOption> items, string preferredKey)
{
if (items == null || items.Count == 0)
{
return null;
}
if (!string.IsNullOrWhiteSpace(preferredKey))
{
var matchingItem = items.FirstOrDefault(delegate(GroupOption item)
{
return item != null
&& string.Equals(item.Key, preferredKey.Trim(), StringComparison.OrdinalIgnoreCase);
});
if (matchingItem != null)
{
return matchingItem;
}
}
return items[0];
}
private void RaiseCloseRequested(bool? dialogResult) private void RaiseCloseRequested(bool? dialogResult)
{ {
var handler = CloseRequested; var handler = CloseRequested;

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Windows; using System.Windows;
@@ -13,6 +14,8 @@ namespace XLAB2
IReadOnlyList<string> ShowCloneVerificationDialog(CloneVerificationSeed seed); IReadOnlyList<string> ShowCloneVerificationDialog(CloneVerificationSeed seed);
GroupOption ShowAccountingBookEditDialog(GroupOption seed, bool isNew, IReadOnlyList<GroupOption> existingItems);
SpoiDirectoryItem ShowSpoiEditDialog(SpoiDirectoryItem seed, bool isNew, IReadOnlyList<SpoiDirectoryItem> existingItems); SpoiDirectoryItem ShowSpoiEditDialog(SpoiDirectoryItem seed, bool isNew, IReadOnlyList<SpoiDirectoryItem> existingItems);
SpnmtpDirectoryItem ShowSpnmtpEditDialog(SpnmtpDirectoryItem seed, bool isNew, IReadOnlyList<SpnmtpDirectoryItem> existingItems); SpnmtpDirectoryItem ShowSpnmtpEditDialog(SpnmtpDirectoryItem seed, bool isNew, IReadOnlyList<SpnmtpDirectoryItem> existingItems);
@@ -33,6 +36,8 @@ namespace XLAB2
internal sealed class DialogService : IDialogService internal sealed class DialogService : IDialogService
{ {
private readonly DocumentNumberDirectoryService _documentNumberDirectoryService;
public DialogService() public DialogService()
{ {
} }
@@ -42,11 +47,18 @@ namespace XLAB2
Owner = owner; Owner = owner;
} }
public DialogService(Window owner, DocumentNumberDirectoryService documentNumberDirectoryService)
{
Owner = owner;
_documentNumberDirectoryService = documentNumberDirectoryService;
}
public Window Owner { get; set; } public Window Owner { get; set; }
public DocumentEditorResult ShowCreateDocumentDialog(DocumentEditorResult seed) public DocumentEditorResult ShowCreateDocumentDialog(DocumentEditorResult seed)
{ {
var viewModel = new CreateDocumentWindowViewModel(seed); var directory = (_documentNumberDirectoryService ?? CreateDefaultDocumentNumberDirectoryService()).LoadDirectory();
var viewModel = new CreateDocumentWindowViewModel(seed, directory);
var window = new CreateDocumentWindow(viewModel); var window = new CreateDocumentWindow(viewModel);
AttachOwner(window); AttachOwner(window);
@@ -84,6 +96,16 @@ namespace XLAB2
return result.HasValue && result.Value ? viewModel.GetSerialNumbers() : null; return result.HasValue && result.Value ? viewModel.GetSerialNumbers() : null;
} }
public GroupOption ShowAccountingBookEditDialog(GroupOption seed, bool isNew, IReadOnlyList<GroupOption> existingItems)
{
var viewModel = new AccountingBookEditWindowViewModel(seed, isNew, existingItems);
var window = new AccountingBookEditWindow(viewModel);
AttachOwner(window);
var result = window.ShowDialog();
return result.HasValue && result.Value ? viewModel.ToResult() : null;
}
public SpoiDirectoryItem ShowSpoiEditDialog(SpoiDirectoryItem seed, bool isNew, IReadOnlyList<SpoiDirectoryItem> existingItems) public SpoiDirectoryItem ShowSpoiEditDialog(SpoiDirectoryItem seed, bool isNew, IReadOnlyList<SpoiDirectoryItem> existingItems)
{ {
var viewModel = new SpoiEditWindowViewModel(seed, isNew, existingItems); var viewModel = new SpoiEditWindowViewModel(seed, isNew, existingItems);
@@ -157,5 +179,10 @@ namespace XLAB2
MessageBox.Show(Owner, message, "ПСВ", MessageBoxButton.OK, image); MessageBox.Show(Owner, message, "ПСВ", MessageBoxButton.OK, image);
} }
private static DocumentNumberDirectoryService CreateDefaultDocumentNumberDirectoryService()
{
return new DocumentNumberDirectoryService(System.IO.Path.Combine(AppContext.BaseDirectory, "Assets", "document-number-directory.json"));
}
} }
} }

View File

@@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace XLAB2
{
internal sealed class DocumentNumberDirectory
{
public DocumentNumberDirectory(IReadOnlyList<GroupOption> accountingBooks, IReadOnlyList<GroupOption> documentTypes)
{
AccountingBooks = accountingBooks ?? Array.Empty<GroupOption>();
DocumentTypes = documentTypes ?? Array.Empty<GroupOption>();
}
public IReadOnlyList<GroupOption> AccountingBooks { get; private set; }
public IReadOnlyList<GroupOption> DocumentTypes { get; private set; }
}
internal sealed class DocumentNumberDirectoryService
{
private readonly string _filePath;
public DocumentNumberDirectoryService(string filePath)
{
_filePath = filePath;
}
public DocumentNumberDirectory LoadDirectory()
{
var configuration = LoadConfiguration();
return new DocumentNumberDirectory(
NormalizeEntries(configuration.AccountingBooks, "книг учета"),
NormalizeEntries(configuration.DocumentTypes, "типов документов"));
}
public IReadOnlyList<GroupOption> LoadAccountingBooks()
{
return LoadDirectory().AccountingBooks;
}
public void SaveAccountingBooks(IReadOnlyList<GroupOption> accountingBooks)
{
var configuration = LoadConfiguration();
var normalizedAccountingBooks = NormalizeWritableEntries(accountingBooks, "книг учета");
var normalizedDocumentTypes = NormalizeEntries(configuration.DocumentTypes, "типов документов");
configuration.AccountingBooks = ToConfigurationEntries(normalizedAccountingBooks);
configuration.DocumentTypes = ToConfigurationEntries(normalizedDocumentTypes);
SaveConfiguration(configuration);
}
private static IReadOnlyList<GroupOption> NormalizeEntries(IEnumerable<DocumentNumberDirectoryEntry> entries, string sectionName)
{
var items = new List<GroupOption>();
var seenKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var entry in entries ?? Array.Empty<DocumentNumberDirectoryEntry>())
{
if (entry == null)
{
continue;
}
var key = DocumentNumberFormatter.NormalizePart(entry.Key);
var title = DocumentNumberFormatter.NormalizePart(entry.Title);
if (key == null || title == null || !seenKeys.Add(key))
{
continue;
}
items.Add(new GroupOption
{
Key = key,
Title = title
});
}
if (items.Count == 0)
{
throw new InvalidOperationException(string.Format("Справочник номеров документов не содержит {0}.", sectionName));
}
return items;
}
private static IReadOnlyList<GroupOption> NormalizeWritableEntries(IEnumerable<GroupOption> entries, string sectionName)
{
var items = new List<GroupOption>();
var seenKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var seenTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var entry in entries ?? Array.Empty<GroupOption>())
{
if (entry == null)
{
continue;
}
var key = DocumentNumberFormatter.NormalizePart(entry.Key);
if (key == null)
{
throw new InvalidOperationException("Не задан ключ книги учета.");
}
var title = DocumentNumberFormatter.NormalizePart(entry.Title);
if (title == null)
{
throw new InvalidOperationException("Не задан номер книги учета.");
}
if (!seenKeys.Add(key))
{
throw new InvalidOperationException(string.Format("Ключ книги учета \"{0}\" повторяется.", key));
}
if (!seenTitles.Add(title))
{
throw new InvalidOperationException(string.Format("Номер книги учета \"{0}\" повторяется.", title));
}
items.Add(new GroupOption
{
Key = key,
Title = title
});
}
if (items.Count == 0)
{
throw new InvalidOperationException(string.Format("Справочник номеров документов не содержит {0}.", sectionName));
}
return items;
}
private DocumentNumberDirectoryConfiguration LoadConfiguration()
{
if (string.IsNullOrWhiteSpace(_filePath))
{
throw new InvalidOperationException("Не задан путь к справочнику номеров документов.");
}
if (!File.Exists(_filePath))
{
throw new InvalidOperationException(string.Format("Не найден справочник номеров документов: {0}.", _filePath));
}
using (var stream = File.OpenRead(_filePath))
{
var configuration = JsonSerializer.Deserialize<DocumentNumberDirectoryConfiguration>(stream, CreateReadSerializerOptions());
if (configuration == null)
{
throw new InvalidOperationException("Справочник номеров документов пуст или поврежден.");
}
return configuration;
}
}
private void SaveConfiguration(DocumentNumberDirectoryConfiguration configuration)
{
var directoryPath = Path.GetDirectoryName(_filePath);
if (!string.IsNullOrWhiteSpace(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
var json = JsonSerializer.Serialize(configuration, CreateWriteSerializerOptions());
File.WriteAllText(_filePath, json);
}
private static List<DocumentNumberDirectoryEntry> ToConfigurationEntries(IEnumerable<GroupOption> entries)
{
var items = new List<DocumentNumberDirectoryEntry>();
foreach (var entry in entries ?? Array.Empty<GroupOption>())
{
if (entry == null)
{
continue;
}
items.Add(new DocumentNumberDirectoryEntry
{
Key = entry.Key,
Title = entry.Title
});
}
return items;
}
private static JsonSerializerOptions CreateReadSerializerOptions()
{
return new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
}
private static JsonSerializerOptions CreateWriteSerializerOptions()
{
return new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
}
}
internal static class DocumentNumberFormatter
{
public const int MaxLength = 60;
public static string Build(string documentTypeTitle, string accountingBookTitle, string documentSequenceNumber)
{
var normalizedDocumentTypeTitle = NormalizePart(documentTypeTitle);
var normalizedAccountingBookTitle = NormalizePart(accountingBookTitle);
var normalizedDocumentSequenceNumber = NormalizePart(documentSequenceNumber);
if (normalizedDocumentTypeTitle == null
|| normalizedAccountingBookTitle == null
|| normalizedDocumentSequenceNumber == null)
{
return string.Empty;
}
return string.Format("{0} № {1}-{2}", normalizedDocumentTypeTitle, normalizedAccountingBookTitle, normalizedDocumentSequenceNumber);
}
public static string NormalizePart(string value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
}
internal sealed class DocumentNumberDirectoryConfiguration
{
public List<DocumentNumberDirectoryEntry> AccountingBooks { get; set; }
public List<DocumentNumberDirectoryEntry> DocumentTypes { get; set; }
[JsonExtensionData]
public Dictionary<string, JsonElement> ExtensionData { get; set; }
}
internal sealed class DocumentNumberDirectoryEntry
{
public string Key { get; set; }
public string Title { get; set; }
}
}

View File

@@ -37,6 +37,8 @@
Click="SpoiDirectoryMenuItem_Click" /> Click="SpoiDirectoryMenuItem_Click" />
<MenuItem Header="Наименования типов СИ" <MenuItem Header="Наименования типов СИ"
Click="SpnmtpDirectoryMenuItem_Click" /> Click="SpnmtpDirectoryMenuItem_Click" />
<MenuItem Header="Книги учета"
Click="AccountingBookDirectoryMenuItem_Click" />
</MenuItem> </MenuItem>
<MenuItem Header="Подразделения" <MenuItem Header="Подразделения"
Click="FrpdDirectoryMenuItem_Click" /> Click="FrpdDirectoryMenuItem_Click" />

View File

@@ -6,12 +6,14 @@ namespace XLAB2
{ {
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private readonly DocumentNumberDirectoryService _documentNumberDirectoryService;
private readonly MainWindowViewModel _viewModel; private readonly MainWindowViewModel _viewModel;
internal MainWindow(PsvDataService service) internal MainWindow(PsvDataService service, DocumentNumberDirectoryService documentNumberDirectoryService)
{ {
InitializeComponent(); InitializeComponent();
_viewModel = new MainWindowViewModel(service, new DialogService(this)); _documentNumberDirectoryService = documentNumberDirectoryService;
_viewModel = new MainWindowViewModel(service, new DialogService(this, documentNumberDirectoryService));
DataContext = _viewModel; DataContext = _viewModel;
} }
@@ -79,6 +81,13 @@ namespace XLAB2
window.ShowDialog(); window.ShowDialog();
} }
private void AccountingBookDirectoryMenuItem_Click(object sender, RoutedEventArgs e)
{
var window = new AccountingBookDirectoryWindow(_documentNumberDirectoryService);
window.Owner = this;
window.ShowDialog();
}
private void TypeSizeDirectoryMenuItem_Click(object sender, RoutedEventArgs e) private void TypeSizeDirectoryMenuItem_Click(object sender, RoutedEventArgs e)
{ {
var window = new TypeSizeDirectoryWindow(); var window = new TypeSizeDirectoryWindow();

View File

@@ -517,8 +517,18 @@ namespace XLAB2
public sealed class DocumentEditorResult public sealed class DocumentEditorResult
{ {
public string AccountingBookKey { get; set; }
public string AccountingBookTitle { get; set; }
public string DocumentNumber { get; set; } public string DocumentNumber { get; set; }
public string DocumentSequenceNumber { get; set; }
public string DocumentTypeKey { get; set; }
public string DocumentTypeTitle { get; set; }
public DateTime AcceptedOn { get; set; } public DateTime AcceptedOn { get; set; }
public DateTime? IssuedOn { get; set; } public DateTime? IssuedOn { get; set; }

View File

@@ -32,6 +32,9 @@
<Content Include="appsettings.json"> <Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Assets\document-number-directory.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="ClosePsv.docx"> <Content Include="ClosePsv.docx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>