edit
This commit is contained in:
118
XLAB2/AccountingBookDirectoryWindow.xaml
Normal file
118
XLAB2/AccountingBookDirectoryWindow.xaml
Normal 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>
|
||||||
35
XLAB2/AccountingBookDirectoryWindow.xaml.cs
Normal file
35
XLAB2/AccountingBookDirectoryWindow.xaml.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
266
XLAB2/AccountingBookDirectoryWindowViewModel.cs
Normal file
266
XLAB2/AccountingBookDirectoryWindowViewModel.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
XLAB2/AccountingBookEditWindow.xaml
Normal file
64
XLAB2/AccountingBookEditWindow.xaml
Normal 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>
|
||||||
20
XLAB2/AccountingBookEditWindow.xaml.cs
Normal file
20
XLAB2/AccountingBookEditWindow.xaml.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
XLAB2/AccountingBookEditWindowViewModel.cs
Normal file
138
XLAB2/AccountingBookEditWindowViewModel.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) =>
|
||||||
{
|
{
|
||||||
|
|||||||
18
XLAB2/Assets/document-number-directory.json
Normal file
18
XLAB2/Assets/document-number-directory.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"documentTypes": [
|
||||||
|
{
|
||||||
|
"key": "PSV",
|
||||||
|
"title": "ПСВ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ACT_REFERENCE",
|
||||||
|
"title": "Акт-справка"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accountingBooks": [
|
||||||
|
{
|
||||||
|
"key": "219/С5/15",
|
||||||
|
"title": "219/С5/15"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
DisplayMemberPath="Title"
|
||||||
Text="{Binding DocumentNumber, UpdateSourceTrigger=PropertyChanged}" />
|
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">
|
||||||
|
|||||||
@@ -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,17 +37,71 @@ 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; }
|
||||||
|
|
||||||
public ICommand ConfirmCommand { get; private set; }
|
public ICommand ConfirmCommand { get; private set; }
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
260
XLAB2/DocumentNumberDirectoryService.cs
Normal file
260
XLAB2/DocumentNumberDirectoryService.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user