This commit is contained in:
Курнат Андрей
2026-03-14 00:35:24 +03:00
parent d4cc15a7cf
commit a0f50842f5
8 changed files with 468 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
<Window x:Class="XLAB.CloneVerificationWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Клонирование поверки"
Height="420"
Width="620"
MinHeight="360"
MinWidth="540"
WindowStartupLocation="CenterOwner">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock FontWeight="SemiBold"
Text="{Binding SourceSerialNumber, StringFormat=Источник: зав. № {0}}" />
<TextBlock Grid.Row="1"
Margin="0,8,0,0"
TextWrapping="Wrap"
Text="{Binding VerificationSummary}" />
<TextBlock Grid.Row="2"
Margin="0,12,0,6"
Text="Введите заводские номера строк, в которые нужно скопировать поверку. Поддерживаются разделители: новая строка, табуляция, запятая, точка с запятой." />
<TextBox Grid.Row="3"
AcceptsReturn="True"
VerticalScrollBarVisibility="Auto"
TextWrapping="Wrap"
Text="{Binding SerialNumbersText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="4"
Margin="0,8,0,0"
Foreground="DimGray"
Text="{Binding StatusText}" />
<StackPanel Grid.Row="5"
Margin="0,12,0,0"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Width="110"
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 XLAB
{
public partial class CloneVerificationWindow : Window
{
internal CloneVerificationWindow(CloneVerificationWindowViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
viewModel.CloseRequested += ViewModelOnCloseRequested;
}
private void ViewModelOnCloseRequested(object sender, bool? dialogResult)
{
DialogResult = dialogResult;
Close();
}
}
}

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Windows.Input;
namespace XLAB
{
internal sealed class CloneVerificationWindowViewModel : ObservableObject
{
private string _serialNumbersText;
private string _statusText;
public CloneVerificationWindowViewModel(CloneVerificationSeed seed)
{
if (seed == null)
{
throw new ArgumentNullException("seed");
}
SourceSerialNumber = string.IsNullOrWhiteSpace(seed.SourceSerialNumber) ? string.Empty : seed.SourceSerialNumber.Trim();
VerificationSummary = seed.VerificationSummary ?? string.Empty;
ConfirmCommand = new RelayCommand(Confirm, CanConfirm);
CancelCommand = new RelayCommand(Cancel);
UpdateStatus();
}
public event EventHandler<bool?> CloseRequested;
public ICommand CancelCommand { get; private set; }
public ICommand ConfirmCommand { get; private set; }
public string SourceSerialNumber { get; private set; }
public string SerialNumbersText
{
get { return _serialNumbersText; }
set
{
if (SetProperty(ref _serialNumbersText, value))
{
UpdateStatus();
((RelayCommand)ConfirmCommand).RaiseCanExecuteChanged();
}
}
}
public string StatusText
{
get { return _statusText; }
private set { SetProperty(ref _statusText, value); }
}
public string VerificationSummary { get; private set; }
public IReadOnlyList<string> GetSerialNumbers()
{
return ParseSerialNumbers(SerialNumbersText);
}
private void Cancel(object parameter)
{
RaiseCloseRequested(false);
}
private bool CanConfirm(object parameter)
{
return ParseSerialNumbers(SerialNumbersText).Count > 0;
}
private void Confirm(object parameter)
{
RaiseCloseRequested(true);
}
private static List<string> ParseSerialNumbers(string value)
{
var serialNumbers = new List<string>();
var unique = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(value))
{
return serialNumbers;
}
var separators = new[] { '\r', '\n', '\t', ',', ';' };
foreach (var token in value.Split(separators, StringSplitOptions.RemoveEmptyEntries))
{
var serialNumber = token.Trim();
if (serialNumber.Length == 0 || !unique.Add(serialNumber))
{
continue;
}
serialNumbers.Add(serialNumber);
}
return serialNumbers;
}
private void RaiseCloseRequested(bool? dialogResult)
{
var handler = CloseRequested;
if (handler != null)
{
handler(this, dialogResult);
}
}
private void UpdateStatus()
{
var serialCount = ParseSerialNumbers(SerialNumbersText).Count;
StatusText = string.Format("Уникальных заводских номеров: {0}.", serialCount);
}
}
}

View File

@@ -11,6 +11,8 @@ namespace XLAB
InstrumentTypeSelectionResult ShowInstrumentTypeDialog(string customerName, IReadOnlyList<AvailableInstrumentItem> instrumentTypes);
IReadOnlyList<string> ShowCloneVerificationDialog(CloneVerificationSeed seed);
VerificationEditResult ShowVerificationDialog(
VerificationEditSeed seed,
IReadOnlyList<PersonReference> verifiers,
@@ -64,6 +66,16 @@ namespace XLAB
return result.HasValue && result.Value ? viewModel.GetResult() : null;
}
public IReadOnlyList<string> ShowCloneVerificationDialog(CloneVerificationSeed seed)
{
var viewModel = new CloneVerificationWindowViewModel(seed);
var window = new CloneVerificationWindow(viewModel);
window.Owner = _owner;
var result = window.ShowDialog();
return result.HasValue && result.Value ? viewModel.GetSerialNumbers() : null;
}
public VerificationEditResult ShowVerificationDialog(
VerificationEditSeed seed,
IReadOnlyList<PersonReference> verifiers,

View File

@@ -272,6 +272,9 @@
HeadersVisibility="Column">
<DataGrid.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Клонировать поверку по зав. №..."
Command="{Binding CloneLineVerificationCommand}" />
<Separator/>
<MenuItem Header="Годен"
Command="{Binding MarkLinePassedCommand}" />
<MenuItem Header="Забракован"

View File

@@ -48,6 +48,7 @@ namespace XLAB
DocumentLinesView.Filter = FilterDocumentLines;
AddDocumentCommand = new RelayCommand(delegate { AddDocument(); }, delegate { return !IsBusy; });
CloneLineVerificationCommand = new RelayCommand(delegate { CloneSelectedLineVerificationAsync(); }, delegate { return CanCloneSelectedLineVerification(); });
DeleteDocumentCommand = new RelayCommand(delegate { DeleteDocumentAsync(); }, delegate { return !IsBusy && SelectedDocument != null; });
DeleteSelectedGroupsCommand = new RelayCommand(delegate { DeleteSelectedGroupsAsync(); }, delegate { return CanDeleteSelectedGroups(); });
MarkLinePassedCommand = new RelayCommand(delegate { EditLineVerificationAsync(true); }, delegate { return CanEditSelectedLineVerification(); });
@@ -64,6 +65,8 @@ namespace XLAB
public ICommand AddDocumentCommand { get; private set; }
public ICommand CloneLineVerificationCommand { get; private set; }
public ObservableCollection<CustomerReference> Customers { get; private set; }
public string DocumentFilterText
@@ -333,6 +336,11 @@ namespace XLAB
&& targetLines.All(delegate(PsvDocumentLine line) { return !HasVerificationData(line); });
}
private bool CanCloneSelectedLineVerification()
{
return !IsBusy && CanUseLineAsCloneSource(SelectedDocumentLine);
}
private bool CanResetSelectedLineVerification()
{
var targetLines = GetVerificationTargetLines();
@@ -355,6 +363,34 @@ namespace XLAB
|| !string.IsNullOrWhiteSpace(line.RejectionReason));
}
private static bool CanUseLineAsCloneSource(PsvDocumentLine line)
{
if (line == null || !line.IsPassed.HasValue || !line.VerifierId.HasValue)
{
return false;
}
if (!line.VerificationPerformedOn.HasValue && !line.VerificationDocumentDate.HasValue)
{
return false;
}
if (!line.IsPassed.Value
&& (string.IsNullOrWhiteSpace(line.RejectionReason)
|| string.IsNullOrWhiteSpace(line.VerificationDocumentNumber)))
{
return false;
}
if (!string.IsNullOrWhiteSpace(line.VerificationDocumentNumber)
&& (!line.VerificationDocumentFormId.HasValue || !line.VerificationDocumentLinkTypeId.HasValue))
{
return false;
}
return true;
}
private List<PsvDocumentLine> GetCheckedDocumentLines()
{
return DocumentLinesView.Cast<object>()
@@ -510,12 +546,90 @@ namespace XLAB
};
}
private CloneVerificationSeed CreateCloneVerificationSeed(PsvDocumentLine sourceLine)
{
var documentDisplay = string.IsNullOrWhiteSpace(sourceLine.VerificationDocumentDisplay)
? "не указан"
: sourceLine.VerificationDocumentDisplay;
var rejectionPart = sourceLine.IsPassed == false && !string.IsNullOrWhiteSpace(sourceLine.RejectionReason)
? string.Format(" Причина: {0}.", sourceLine.RejectionReason.Trim())
: string.Empty;
return new CloneVerificationSeed
{
SourceSerialNumber = sourceLine.SerialNumber,
VerificationSummary = string.Format(
"Результат: {0}. Дата поверки: {1:d}. Поверитель: {2}. Документ: {3}.{4}",
sourceLine.ResultText,
sourceLine.VerificationPerformedOn ?? sourceLine.VerificationDocumentDate ?? DateTime.Today,
string.IsNullOrWhiteSpace(sourceLine.VerifierName) ? "не указан" : sourceLine.VerifierName,
documentDisplay,
rejectionPart)
};
}
private VerificationEditResult CreateVerificationResultFromLine(PsvDocumentLine sourceLine)
{
return new VerificationEditResult
{
DocumentFormId = string.IsNullOrWhiteSpace(sourceLine.VerificationDocumentNumber)
? 0
: sourceLine.VerificationDocumentFormId.GetValueOrDefault(),
DocumentLinkTypeId = string.IsNullOrWhiteSpace(sourceLine.VerificationDocumentNumber)
? 0
: sourceLine.VerificationDocumentLinkTypeId.GetValueOrDefault(),
IsPassed = sourceLine.IsPassed.GetValueOrDefault(),
RejectionReason = string.IsNullOrWhiteSpace(sourceLine.RejectionReason) ? string.Empty : sourceLine.RejectionReason.Trim(),
StickerNumber = string.IsNullOrWhiteSpace(sourceLine.StickerNumber) ? string.Empty : sourceLine.StickerNumber.Trim(),
VerificationDate = sourceLine.VerificationPerformedOn ?? sourceLine.VerificationDocumentDate ?? DateTime.Today,
VerificationDocumentNumber = string.IsNullOrWhiteSpace(sourceLine.VerificationDocumentNumber) ? string.Empty : sourceLine.VerificationDocumentNumber.Trim(),
VerifierId = sourceLine.VerifierId.GetValueOrDefault(),
VerifierName = string.IsNullOrWhiteSpace(sourceLine.VerifierName) ? string.Empty : sourceLine.VerifierName
};
}
private bool Contains(string source, string filter)
{
return !string.IsNullOrWhiteSpace(source)
&& source.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool BelongsToSameGroup(PsvDocumentLine candidate, PsvDocumentLine sourceLine)
{
return candidate != null
&& sourceLine != null
&& string.Equals(candidate.InstrumentType ?? string.Empty, sourceLine.InstrumentType ?? string.Empty, StringComparison.OrdinalIgnoreCase)
&& string.Equals(candidate.RangeText ?? string.Empty, sourceLine.RangeText ?? string.Empty, StringComparison.OrdinalIgnoreCase)
&& string.Equals(candidate.RegistryNumber ?? string.Empty, sourceLine.RegistryNumber ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
private static string NormalizeSerialNumber(string serialNumber)
{
return string.IsNullOrWhiteSpace(serialNumber) ? string.Empty : serialNumber.Trim();
}
private static string BuildSerialNumberPreview(IEnumerable<string> serialNumbers)
{
var materialized = serialNumbers == null
? new List<string>()
: serialNumbers
.Where(delegate(string serialNumber) { return !string.IsNullOrWhiteSpace(serialNumber); })
.ToList();
var preview = materialized.Take(5).ToList();
if (preview.Count == 0)
{
return string.Empty;
}
var text = string.Join(", ", preview);
if (materialized.Count > preview.Count)
{
text += ", ...";
}
return text;
}
private string BuildOpenDocumentConflictMessage(IEnumerable<OpenDocumentConflictInfo> conflicts)
{
var materializedConflicts = NormalizeOpenDocumentConflicts(conflicts);
@@ -764,6 +878,136 @@ namespace XLAB
RefreshAfterLineVerificationChanged(line);
}
private void CloneSelectedLineVerificationAsync()
{
var sourceLine = SelectedDocumentLine;
if (!CanUseLineAsCloneSource(sourceLine))
{
_dialogService.ShowWarning("В выбранной строке нет полной поверки, пригодной для клонирования.");
return;
}
var serialNumbers = _dialogService.ShowCloneVerificationDialog(CreateCloneVerificationSeed(sourceLine));
if (serialNumbers == null || serialNumbers.Count == 0)
{
return;
}
var sourceSerialNumber = NormalizeSerialNumber(sourceLine.SerialNumber);
var requestedSerialNumbers = new HashSet<string>(serialNumbers, StringComparer.OrdinalIgnoreCase);
var includedSourceSerialNumber = !string.IsNullOrWhiteSpace(sourceSerialNumber)
&& requestedSerialNumbers.Remove(sourceSerialNumber);
var matchedLines = DocumentLines
.Where(delegate(PsvDocumentLine line)
{
return line != null
&& !ReferenceEquals(line, sourceLine)
&& BelongsToSameGroup(line, sourceLine)
&& requestedSerialNumbers.Contains(NormalizeSerialNumber(line.SerialNumber));
})
.ToList();
var matchedSerialNumbers = new HashSet<string>(
matchedLines
.Select(delegate(PsvDocumentLine line) { return NormalizeSerialNumber(line.SerialNumber); })
.Where(delegate(string serialNumber) { return !string.IsNullOrWhiteSpace(serialNumber); }),
StringComparer.OrdinalIgnoreCase);
var missingSerialNumbers = serialNumbers
.Where(delegate(string serialNumber)
{
var normalized = NormalizeSerialNumber(serialNumber);
return !string.IsNullOrWhiteSpace(normalized)
&& !string.Equals(normalized, sourceSerialNumber, StringComparison.OrdinalIgnoreCase)
&& !matchedSerialNumbers.Contains(normalized);
})
.ToList();
var targetLines = matchedLines.Where(delegate(PsvDocumentLine line) { return !HasVerificationData(line); }).ToList();
var skippedWithExistingVerificationCount = matchedLines.Count - targetLines.Count;
if (targetLines.Count == 0)
{
ShowCloneVerificationResult(0, skippedWithExistingVerificationCount, missingSerialNumbers, includedSourceSerialNumber, true);
return;
}
var result = CreateVerificationResultFromLine(sourceLine);
var pendingLines = targetLines.Where(delegate(PsvDocumentLine line) { return line.IsPendingInsert; }).ToList();
var persistedCardIds = targetLines
.Where(delegate(PsvDocumentLine line) { return !line.IsPendingInsert; })
.Select(delegate(PsvDocumentLine line) { return line.CardId; })
.Distinct()
.ToList();
if (persistedCardIds.Count == 0)
{
foreach (var pendingLine in pendingLines)
{
ApplyVerificationResultCore(pendingLine, result);
}
RefreshAfterLineVerificationChanged(sourceLine);
ShowCloneVerificationResult(targetLines.Count, skippedWithExistingVerificationCount, missingSerialNumbers, includedSourceSerialNumber, false);
return;
}
RunBusyOperation(async delegate
{
await Task.Run(delegate { _service.SaveLineVerification(persistedCardIds, result); });
foreach (var pendingLine in pendingLines)
{
ApplyVerificationResultCore(pendingLine, result);
}
await ReloadSelectedDocumentLinesAsync();
ShowCloneVerificationResult(targetLines.Count, skippedWithExistingVerificationCount, missingSerialNumbers, includedSourceSerialNumber, false);
});
}
private void ShowCloneVerificationResult(
int clonedCount,
int skippedWithExistingVerificationCount,
IReadOnlyList<string> missingSerialNumbers,
bool includedSourceSerialNumber,
bool showWarning)
{
var messages = new List<string>();
if (clonedCount > 0)
{
messages.Add(string.Format("Клонировано поверок: {0}.", clonedCount));
}
if (skippedWithExistingVerificationCount > 0)
{
messages.Add(string.Format("Пропущено строк с уже заполненной поверкой: {0}.", skippedWithExistingVerificationCount));
}
if (includedSourceSerialNumber)
{
messages.Add("Номер строки-источника пропущен.");
}
if (missingSerialNumbers != null && missingSerialNumbers.Count > 0)
{
messages.Add(string.Format("Не найдены заводские номера: {0}.", BuildSerialNumberPreview(missingSerialNumbers)));
}
if (messages.Count == 0)
{
messages.Add("Подходящих строк для клонирования не найдено.");
}
var message = string.Join(" ", messages.ToArray());
if (showWarning || clonedCount == 0)
{
_dialogService.ShowWarning(message);
return;
}
_dialogService.ShowInfo(message);
}
private void EditLineVerificationAsync(bool isPassed)
{
var targetLines = GetVerificationTargetLines();
@@ -1579,6 +1823,7 @@ namespace XLAB
private void RaiseCommandStates()
{
((RelayCommand)AddDocumentCommand).RaiseCanExecuteChanged();
((RelayCommand)CloneLineVerificationCommand).RaiseCanExecuteChanged();
((RelayCommand)DeleteDocumentCommand).RaiseCanExecuteChanged();
((RelayCommand)DeleteSelectedGroupsCommand).RaiseCanExecuteChanged();
((RelayCommand)MarkLinePassedCommand).RaiseCanExecuteChanged();

View File

@@ -395,6 +395,13 @@ namespace XLAB
public int? VerifierId { get; set; }
}
public sealed class CloneVerificationSeed
{
public string SourceSerialNumber { get; set; }
public string VerificationSummary { get; set; }
}
public sealed class VerificationEditResult
{
public int DocumentFormId { get; set; }

View File

@@ -68,6 +68,15 @@
<SubType>Code</SubType>
</Compile>
<Compile Include="CreateDocumentWindowViewModel.cs" />
<Page Include="CloneVerificationWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="CloneVerificationWindow.xaml.cs">
<DependentUpon>CloneVerificationWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="CloneVerificationWindowViewModel.cs" />
<Compile Include="DialogService.cs" />
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>