diff --git a/XLAB/MainWindow.xaml b/XLAB/MainWindow.xaml
index ba3789f..13a0139 100644
--- a/XLAB/MainWindow.xaml
+++ b/XLAB/MainWindow.xaml
@@ -46,6 +46,8 @@
Click="TypeSizeDirectoryMenuItem_Click" />
+
diff --git a/XLAB/MainWindow.xaml.cs b/XLAB/MainWindow.xaml.cs
index b82cd8f..135b1e1 100644
--- a/XLAB/MainWindow.xaml.cs
+++ b/XLAB/MainWindow.xaml.cs
@@ -100,6 +100,13 @@ namespace XLAB
window.ShowDialog();
}
+ private void PlanningMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ var window = new PlanningWindow();
+ window.Owner = this;
+ window.ShowDialog();
+ }
+
private void SpoiDirectoryMenuItem_Click(object sender, RoutedEventArgs e)
{
var window = new SpoiDirectoryWindow();
diff --git a/XLAB/PlanningDialogService.cs b/XLAB/PlanningDialogService.cs
new file mode 100644
index 0000000..e8f6e0b
--- /dev/null
+++ b/XLAB/PlanningDialogService.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using System.Windows;
+
+namespace XLAB
+{
+ internal interface IPlanningDialogService
+ {
+ PlanningEditResult ShowPlanningEditDialog(PlanningEditSeed seed, bool isNew, IReadOnlyList instruments, PlanningService service);
+
+ EkzDirectoryItem ShowEkzEditDialog(EkzDirectoryItem seed, bool isNew, IReadOnlyList existingItems, EkzDirectoryService service);
+
+ bool Confirm(string message);
+
+ void ShowError(string message);
+
+ void ShowInfo(string message);
+
+ void ShowWarning(string message);
+ }
+
+ internal sealed class PlanningDialogService : IPlanningDialogService
+ {
+ private readonly Window _owner;
+
+ public PlanningDialogService(Window owner)
+ {
+ _owner = owner;
+ }
+
+ public bool Confirm(string message)
+ {
+ return MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
+ }
+
+ public EkzDirectoryItem ShowEkzEditDialog(EkzDirectoryItem seed, bool isNew, IReadOnlyList existingItems, EkzDirectoryService service)
+ {
+ var viewModel = new EkzEditWindowViewModel(seed, isNew, existingItems, service);
+ var window = new EkzEditWindow(viewModel);
+ window.Owner = _owner;
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.ToResult() : null;
+ }
+
+ public void ShowError(string message)
+ {
+ MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+
+ public void ShowInfo(string message)
+ {
+ MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+
+ public PlanningEditResult ShowPlanningEditDialog(PlanningEditSeed seed, bool isNew, IReadOnlyList instruments, PlanningService service)
+ {
+ var viewModel = new PlanningEditWindowViewModel(seed, isNew, instruments, service);
+ var window = new PlanningEditWindow(viewModel);
+ window.Owner = _owner;
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.ToResult() : null;
+ }
+
+ public void ShowWarning(string message)
+ {
+ MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ }
+}
diff --git a/XLAB/PlanningEditWindow.xaml b/XLAB/PlanningEditWindow.xaml
new file mode 100644
index 0000000..f07613c
--- /dev/null
+++ b/XLAB/PlanningEditWindow.xaml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XLAB/PlanningEditWindow.xaml.cs b/XLAB/PlanningEditWindow.xaml.cs
new file mode 100644
index 0000000..73c1f82
--- /dev/null
+++ b/XLAB/PlanningEditWindow.xaml.cs
@@ -0,0 +1,20 @@
+using System.Windows;
+
+namespace XLAB
+{
+ public partial class PlanningEditWindow : Window
+ {
+ internal PlanningEditWindow(PlanningEditWindowViewModel viewModel)
+ {
+ InitializeComponent();
+ DataContext = viewModel;
+ viewModel.CloseRequested += ViewModelOnCloseRequested;
+ }
+
+ private void ViewModelOnCloseRequested(object sender, bool? dialogResult)
+ {
+ DialogResult = dialogResult;
+ Close();
+ }
+ }
+}
diff --git a/XLAB/PlanningEditWindowViewModel.cs b/XLAB/PlanningEditWindowViewModel.cs
new file mode 100644
index 0000000..a3be810
--- /dev/null
+++ b/XLAB/PlanningEditWindowViewModel.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Input;
+
+namespace XLAB
+{
+ internal sealed class PlanningEditWindowViewModel : ObservableObject
+ {
+ private readonly IReadOnlyList _instrumentItems;
+ private readonly bool _isNew;
+ private readonly int? _seedTemplateId;
+ private readonly PlanningService _service;
+ private int _selectedInstrumentId;
+ private int? _selectedTemplateId;
+ private IReadOnlyList _templateItems;
+ private DateTime? _plannedOn;
+ private string _validationMessage;
+
+ public PlanningEditWindowViewModel(PlanningEditSeed seed, bool isNew, IReadOnlyList instruments, PlanningService service)
+ {
+ if (seed == null)
+ {
+ throw new ArgumentNullException("seed");
+ }
+
+ _instrumentItems = instruments ?? Array.Empty();
+ _service = service ?? throw new ArgumentNullException("service");
+ _isNew = isNew;
+ _seedTemplateId = seed.TemplateId;
+
+ PlanId = seed.PlanId;
+ TargetYear = seed.TargetYear;
+ InstrumentItems = _instrumentItems
+ .OrderBy(delegate(PlanningInstrumentOption item) { return item.DisplayName; }, StringComparer.CurrentCultureIgnoreCase)
+ .ToList();
+ TemplateItems = Array.Empty();
+ PlannedOn = seed.PlannedOn;
+
+ ConfirmCommand = new RelayCommand(Confirm);
+ CancelCommand = new RelayCommand(Cancel);
+
+ if (seed.InstrumentId > 0)
+ {
+ SelectedInstrumentId = seed.InstrumentId;
+ }
+ else if (InstrumentItems.Count > 0)
+ {
+ SelectedInstrumentId = InstrumentItems[0].Id;
+ }
+ }
+
+ public event EventHandler CloseRequested;
+
+ public ICommand CancelCommand { get; private set; }
+
+ public ICommand ConfirmCommand { get; private set; }
+
+ public IReadOnlyList InstrumentItems { get; private set; }
+
+ public int? PlanId { get; private set; }
+
+ public DateTime? PlannedOn
+ {
+ get { return _plannedOn; }
+ set { SetProperty(ref _plannedOn, value); }
+ }
+
+ public int SelectedInstrumentId
+ {
+ get { return _selectedInstrumentId; }
+ set
+ {
+ if (SetProperty(ref _selectedInstrumentId, value))
+ {
+ LoadTemplates();
+ OnPropertyChanged("SelectedInstrumentDescription");
+ OnPropertyChanged("TemplateWarningMessage");
+ }
+ }
+ }
+
+ public PlanningInstrumentOption SelectedInstrument
+ {
+ get { return InstrumentItems.FirstOrDefault(delegate(PlanningInstrumentOption item) { return item.Id == SelectedInstrumentId; }); }
+ }
+
+ public string SelectedInstrumentDescription
+ {
+ get
+ {
+ return SelectedInstrument == null ? string.Empty : SelectedInstrument.DisplayName;
+ }
+ }
+
+ public int? SelectedTemplateId
+ {
+ get { return _selectedTemplateId; }
+ set { SetProperty(ref _selectedTemplateId, value); }
+ }
+
+ public IReadOnlyList TemplateItems
+ {
+ get { return _templateItems; }
+ private set
+ {
+ _templateItems = value ?? Array.Empty();
+ OnPropertyChanged("TemplateItems");
+ OnPropertyChanged("SelectedTemplateDescription");
+ }
+ }
+
+ public string SelectedTemplateDescription
+ {
+ get
+ {
+ var selected = TemplateItems.FirstOrDefault(delegate(PlanningTemplateOption item) { return item.Id == SelectedTemplateId; });
+ return selected == null ? string.Empty : selected.DisplayName;
+ }
+ }
+
+ public string TemplateWarningMessage
+ {
+ get
+ {
+ if (SelectedInstrument == null || TemplateItems.Count > 0)
+ {
+ return string.Empty;
+ }
+
+ return "Для выбранного прибора в TPRMCP не найден период. Расчет можно показать, но запись в EKZMCP сохранить нельзя.";
+ }
+ }
+
+ public int TargetYear { get; private set; }
+
+ public string Title
+ {
+ get
+ {
+ return _isNew || !PlanId.HasValue
+ ? string.Format("Планирование на {0} год", TargetYear)
+ : string.Format("Редактирование плана {0}", TargetYear);
+ }
+ }
+
+ public string ValidationMessage
+ {
+ get { return _validationMessage; }
+ private set { SetProperty(ref _validationMessage, value); }
+ }
+
+ public PlanningEditResult ToResult()
+ {
+ if (!PlannedOn.HasValue || !SelectedTemplateId.HasValue)
+ {
+ throw new InvalidOperationException("Результат плановой записи недоступен без даты и периода.");
+ }
+
+ return new PlanningEditResult
+ {
+ PlanId = PlanId,
+ InstrumentId = SelectedInstrumentId,
+ TemplateId = SelectedTemplateId.Value,
+ PlannedOn = PlannedOn.Value.Date
+ };
+ }
+
+ private void Cancel(object parameter)
+ {
+ RaiseCloseRequested(false);
+ }
+
+ private void Confirm(object parameter)
+ {
+ if (SelectedInstrument == null)
+ {
+ ValidationMessage = "Выберите прибор.";
+ return;
+ }
+
+ if (TemplateItems.Count == 0)
+ {
+ ValidationMessage = "Для выбранного прибора отсутствует период TPRMCP. Сохранить запись в EKZMCP нельзя.";
+ return;
+ }
+
+ if (!SelectedTemplateId.HasValue || TemplateItems.All(delegate(PlanningTemplateOption item) { return item.Id != SelectedTemplateId.Value; }))
+ {
+ ValidationMessage = "Выберите период из TPRMCP.";
+ return;
+ }
+
+ if (!PlannedOn.HasValue)
+ {
+ ValidationMessage = "Укажите плановую дату.";
+ return;
+ }
+
+ if (PlannedOn.Value.Year != TargetYear)
+ {
+ ValidationMessage = string.Format("Плановая дата должна относиться к {0} году.", TargetYear);
+ return;
+ }
+
+ ValidationMessage = string.Empty;
+ RaiseCloseRequested(true);
+ }
+
+ private void LoadTemplates()
+ {
+ var instrument = SelectedInstrument;
+ if (instrument == null)
+ {
+ TemplateItems = Array.Empty();
+ SelectedTemplateId = null;
+ return;
+ }
+
+ var templates = _service.LoadTemplateOptions(instrument.TypeSizeId)
+ .OrderBy(delegate(PlanningTemplateOption item) { return item.PeriodMonths; })
+ .ThenBy(delegate(PlanningTemplateOption item) { return item.DisplayName; }, StringComparer.CurrentCultureIgnoreCase)
+ .ToList();
+
+ TemplateItems = templates;
+
+ if (_seedTemplateId.HasValue && templates.Any(delegate(PlanningTemplateOption item) { return item.Id == _seedTemplateId.Value; }))
+ {
+ SelectedTemplateId = _seedTemplateId.Value;
+ return;
+ }
+
+ if (SelectedTemplateId.HasValue && templates.Any(delegate(PlanningTemplateOption item) { return item.Id == SelectedTemplateId.Value; }))
+ {
+ return;
+ }
+
+ SelectedTemplateId = templates.Count > 0 ? (int?)templates[0].Id : null;
+ }
+
+ private void RaiseCloseRequested(bool? dialogResult)
+ {
+ var handler = CloseRequested;
+ if (handler != null)
+ {
+ handler(this, dialogResult);
+ }
+ }
+ }
+}
diff --git a/XLAB/PlanningModels.cs b/XLAB/PlanningModels.cs
new file mode 100644
index 0000000..e895a81
--- /dev/null
+++ b/XLAB/PlanningModels.cs
@@ -0,0 +1,157 @@
+using System;
+
+namespace XLAB
+{
+ public sealed class PlanningItem
+ {
+ public int InstrumentId { get; set; }
+
+ public int TypeSizeId { get; set; }
+
+ public int OwnerOrganizationId { get; set; }
+
+ public string OwnerOrganizationName { get; set; }
+
+ public string MeasurementAreaName { get; set; }
+
+ public string InstrumentName { get; set; }
+
+ public string TypeName { get; set; }
+
+ public string RangeText { get; set; }
+
+ public string RegistryNumber { get; set; }
+
+ public string SerialNumber { get; set; }
+
+ public string InventoryNumber { get; set; }
+
+ public int? PlanId { get; set; }
+
+ public int? EffectiveTemplateId { get; set; }
+
+ public int? PeriodMonths { get; set; }
+
+ public DateTime? LastVerificationOn { get; set; }
+
+ public DateTime? PlannedOn { get; set; }
+
+ public bool IsExplicitPlan { get; set; }
+
+ public string PlanSource { get; set; }
+
+ public string PeriodSource { get; set; }
+
+ public bool CanPersistPlan
+ {
+ get { return PlanId.HasValue || EffectiveTemplateId.HasValue; }
+ }
+
+ public string PeriodDisplay
+ {
+ get { return PeriodMonths.HasValue ? string.Format("{0} мес.", PeriodMonths.Value) : string.Empty; }
+ }
+
+ public string RecordKindText
+ {
+ get { return IsExplicitPlan ? "EKZMCP" : "Расчет"; }
+ }
+
+ public string PersistenceText
+ {
+ get { return CanPersistPlan ? string.Empty : "Только расчет"; }
+ }
+ }
+
+ internal sealed class PlanningInstrumentOption
+ {
+ public int Id { get; set; }
+
+ public int TypeSizeId { get; set; }
+
+ public int OwnerOrganizationId { get; set; }
+
+ public string DisplayName { get; set; }
+
+ public override string ToString()
+ {
+ return DisplayName ?? string.Empty;
+ }
+ }
+
+ internal sealed class PlanningTemplateOption
+ {
+ public int Id { get; set; }
+
+ public int TypeSizeId { get; set; }
+
+ public int? CycleId { get; set; }
+
+ public string CycleName { get; set; }
+
+ public int? GroupId { get; set; }
+
+ public string GroupName { get; set; }
+
+ public int PeriodMonths { get; set; }
+
+ public string Comment { get; set; }
+
+ public string DisplayName
+ {
+ get
+ {
+ var parts = new System.Collections.Generic.List
+ {
+ string.Format("{0} мес.", PeriodMonths)
+ };
+
+ if (!string.IsNullOrWhiteSpace(CycleName))
+ {
+ parts.Add(CycleName.Trim());
+ }
+
+ if (!string.IsNullOrWhiteSpace(GroupName))
+ {
+ parts.Add(GroupName.Trim());
+ }
+
+ if (!string.IsNullOrWhiteSpace(Comment))
+ {
+ parts.Add(Comment.Trim());
+ }
+
+ return string.Join(" / ", parts.ToArray());
+ }
+ }
+
+ public override string ToString()
+ {
+ return DisplayName;
+ }
+ }
+
+ internal sealed class PlanningEditSeed
+ {
+ public int TargetYear { get; set; }
+
+ public int? PlanId { get; set; }
+
+ public int InstrumentId { get; set; }
+
+ public int? TemplateId { get; set; }
+
+ public DateTime? PlannedOn { get; set; }
+ }
+
+ internal sealed class PlanningEditResult
+ {
+ public int? PlanId { get; set; }
+
+ public int InstrumentId { get; set; }
+
+ public int TemplateId { get; set; }
+
+ public DateTime PlannedOn { get; set; }
+ }
+}
diff --git a/XLAB/PlanningService.cs b/XLAB/PlanningService.cs
new file mode 100644
index 0000000..ccc4605
--- /dev/null
+++ b/XLAB/PlanningService.cs
@@ -0,0 +1,394 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.SqlClient;
+
+namespace XLAB
+{
+ internal sealed class PlanningService
+ {
+ public int AddPlanItem(PlanningEditResult item)
+ {
+ if (item == null)
+ {
+ throw new InvalidOperationException("Не переданы данные плановой записи.");
+ }
+
+ const string sql = @"
+INSERT INTO dbo.EKZMCP
+(
+ IDEKZ,
+ IDTPRMCP,
+ DTMKPLO,
+ PZMCO,
+ IDSPVDMK
+)
+VALUES
+(
+ @InstrumentId,
+ @TemplateId,
+ @PlannedOn,
+ NULL,
+ NULL
+);
+
+SELECT CAST(SCOPE_IDENTITY() AS int);";
+
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ using (var command = new SqlCommand(sql, connection))
+ {
+ connection.Open();
+ EnsurePlanIsUnique(connection, item.InstrumentId, item.TemplateId, null);
+
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@InstrumentId", SqlDbType.Int).Value = item.InstrumentId;
+ command.Parameters.Add("@TemplateId", SqlDbType.Int).Value = item.TemplateId;
+ command.Parameters.Add("@PlannedOn", SqlDbType.DateTime).Value = item.PlannedOn.Date;
+
+ try
+ {
+ return Convert.ToInt32(command.ExecuteScalar());
+ }
+ catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "XAK1EKZMCP"))
+ {
+ throw CreatePlanDuplicateException(ex);
+ }
+ }
+ }
+
+ public void DeletePlanItem(int id)
+ {
+ if (id <= 0)
+ {
+ throw new InvalidOperationException("Не выбрана запись EKZMCP для удаления.");
+ }
+
+ const string sql = @"
+DELETE FROM dbo.EKZMCP
+WHERE IDEKZMCP = @Id;
+
+SELECT @@ROWCOUNT;";
+
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ using (var command = new SqlCommand(sql, connection))
+ {
+ connection.Open();
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
+
+ if (Convert.ToInt32(command.ExecuteScalar()) == 0)
+ {
+ throw new InvalidOperationException("Запись EKZMCP для удаления не найдена.");
+ }
+ }
+ }
+
+ public IReadOnlyList LoadOwnerItems()
+ {
+ return ReferenceDirectorySqlHelpers.LoadLookupItems(@"
+SELECT
+ fr.IDFRPD AS Id,
+ fr.NMFRPD AS Name
+FROM dbo.FRPD fr
+WHERE NULLIF(LTRIM(RTRIM(fr.NMFRPD)), '') IS NOT NULL
+ORDER BY fr.NMFRPD, fr.IDFRPD;");
+ }
+
+ public IReadOnlyList LoadPlanItems(int year)
+ {
+ if (year < 2000 || year > 2100)
+ {
+ throw new InvalidOperationException("Год планирования должен быть в диапазоне 2000-2100.");
+ }
+
+ const string sql = @"
+SELECT
+ z.IDEKZ AS InstrumentId,
+ z.IDTPRZ AS TypeSizeId,
+ z.IDFRPDV AS OwnerOrganizationId,
+ ownerOrg.NMFRPD AS OwnerOrganizationName,
+ areas.NMOI AS MeasurementAreaName,
+ names.NMTP AS InstrumentName,
+ tips.TP AS TypeName,
+ tprz.DPZN AS RangeText,
+ CONVERT(nvarchar(50), tprz.NNGSRS) AS RegistryNumber,
+ z.NNZV AS SerialNumber,
+ z.NNIN AS InventoryNumber,
+ lastMk.LastVerificationOn,
+ selectedPlan.IDEKZMCP AS PlanId,
+ CASE
+ WHEN selectedPlan.IDEKZMCP IS NOT NULL THEN selectedPlan.IDTPRMCP
+ ELSE COALESCE(periodByInstrument.IDTPRMCP, periodByType.IDTPRMCP)
+ END AS EffectiveTemplateId,
+ CASE
+ WHEN selectedPlan.IDEKZMCP IS NOT NULL THEN selectedPlan.PRMK
+ ELSE COALESCE(periodByInstrument.PRMK, periodByType.PRMK, tips.PRMKGR)
+ END AS PeriodMonths,
+ CASE
+ WHEN selectedPlan.IDEKZMCP IS NOT NULL THEN selectedPlan.DTMKPLO
+ ELSE DATEADD(month, COALESCE(periodByInstrument.PRMK, periodByType.PRMK, tips.PRMKGR), lastMk.LastVerificationOn)
+ END AS PlannedOn,
+ CASE
+ WHEN selectedPlan.IDEKZMCP IS NOT NULL THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS IsExplicitPlan,
+ CASE
+ WHEN selectedPlan.IDEKZMCP IS NOT NULL THEN N'План из EKZMCP'
+ WHEN periodByInstrument.IDTPRMCP IS NOT NULL THEN N'Расчет по TPRMCP экземпляра'
+ WHEN periodByType.IDTPRMCP IS NOT NULL THEN N'Расчет по TPRMCP типоразмера'
+ WHEN tips.PRMKGR IS NOT NULL THEN N'Расчет по TIPS.PRMKGR'
+ ELSE N''
+ END AS PlanSource,
+ CASE
+ WHEN selectedPlan.IDEKZMCP IS NOT NULL THEN N'EKZMCP / TPRMCP'
+ WHEN periodByInstrument.IDTPRMCP IS NOT NULL THEN N'TPRMCP экземпляра'
+ WHEN periodByType.IDTPRMCP IS NOT NULL THEN N'TPRMCP типоразмера'
+ WHEN tips.PRMKGR IS NOT NULL THEN N'TIPS.PRMKGR'
+ ELSE N''
+ END AS PeriodSource
+FROM dbo.EKZ z
+JOIN dbo.TPRZ tprz ON tprz.IDTPRZ = z.IDTPRZ
+JOIN dbo.TIPS tips ON tips.IDTIPS = tprz.IDTIPS
+JOIN dbo.SPNMTP names ON names.IDSPNMTP = tips.IDSPNMTP
+JOIN dbo.SPOI areas ON areas.IDSPOI = tips.IDSPOI
+JOIN dbo.FRPD ownerOrg ON ownerOrg.IDFRPD = z.IDFRPDV
+OUTER APPLY
+(
+ SELECT TOP (1)
+ COALESCE(m.DTMKFK, m.DTVDM, m.DTPRM) AS LastVerificationOn
+ FROM dbo.EKZMK m
+ WHERE m.IDEKZ = z.IDEKZ
+ AND COALESCE(m.DTMKFK, m.DTVDM, m.DTPRM) IS NOT NULL
+ ORDER BY COALESCE(m.DTMKFK, m.DTVDM, m.DTPRM) DESC, m.IDEKZMK DESC
+) lastMk
+OUTER APPLY
+(
+ SELECT TOP (1)
+ e.IDEKZMCP,
+ e.IDTPRMCP,
+ e.DTMKPLO,
+ t.PRMK
+ FROM dbo.EKZMCP e
+ JOIN dbo.TPRMCP t ON t.IDTPRMCP = e.IDTPRMCP
+ WHERE e.IDEKZ = z.IDEKZ
+ AND e.DTMKPLO >= @YearStart
+ AND e.DTMKPLO < @YearEnd
+ ORDER BY e.DTMKPLO, e.IDEKZMCP DESC
+) selectedPlan
+OUTER APPLY
+(
+ SELECT TOP (1)
+ e.IDEKZMCP AS PlanId
+ FROM dbo.EKZMCP e
+ WHERE e.IDEKZ = z.IDEKZ
+ AND e.DTMKPLO >= @YearEnd
+ ORDER BY e.DTMKPLO, e.IDEKZMCP DESC
+) futurePlan
+OUTER APPLY
+(
+ SELECT TOP (1)
+ e.IDTPRMCP,
+ t.PRMK
+ FROM dbo.EKZMCP e
+ JOIN dbo.TPRMCP t ON t.IDTPRMCP = e.IDTPRMCP
+ WHERE e.IDEKZ = z.IDEKZ
+ ORDER BY e.IDEKZMCP DESC, t.IDTPRMCP DESC
+) periodByInstrument
+OUTER APPLY
+(
+ SELECT TOP (1)
+ t.IDTPRMCP,
+ t.PRMK
+ FROM dbo.TPRMCP t
+ WHERE t.IDTPRZ = z.IDTPRZ
+ ORDER BY t.IDTPRMCP DESC
+) periodByType
+WHERE ISNULL(z.IsDeleted, 0) = 0
+ AND
+ (
+ selectedPlan.IDEKZMCP IS NOT NULL
+ OR
+ (
+ futurePlan.PlanId IS NULL
+ AND lastMk.LastVerificationOn IS NOT NULL
+ AND COALESCE(periodByInstrument.PRMK, periodByType.PRMK, tips.PRMKGR) IS NOT NULL
+ AND DATEADD(month, COALESCE(periodByInstrument.PRMK, periodByType.PRMK, tips.PRMKGR), lastMk.LastVerificationOn) >= @YearStart
+ AND DATEADD(month, COALESCE(periodByInstrument.PRMK, periodByType.PRMK, tips.PRMKGR), lastMk.LastVerificationOn) < @YearEnd
+ )
+ )
+ORDER BY ownerOrg.NMFRPD, areas.NMOI, names.NMTP, tips.TP, tprz.DPZN, z.NNZV, z.IDEKZ;";
+
+ var yearStart = new DateTime(year, 1, 1);
+ var yearEnd = yearStart.AddYears(1);
+ var items = new List();
+
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ using (var command = new SqlCommand(sql, connection))
+ {
+ connection.Open();
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@YearStart", SqlDbType.DateTime).Value = yearStart;
+ command.Parameters.Add("@YearEnd", SqlDbType.DateTime).Value = yearEnd;
+
+ using (var reader = command.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ items.Add(new PlanningItem
+ {
+ InstrumentId = ReferenceDirectorySqlHelpers.GetInt32(reader, "InstrumentId"),
+ TypeSizeId = ReferenceDirectorySqlHelpers.GetInt32(reader, "TypeSizeId"),
+ OwnerOrganizationId = ReferenceDirectorySqlHelpers.GetInt32(reader, "OwnerOrganizationId"),
+ OwnerOrganizationName = ReferenceDirectorySqlHelpers.GetString(reader, "OwnerOrganizationName"),
+ MeasurementAreaName = ReferenceDirectorySqlHelpers.GetString(reader, "MeasurementAreaName"),
+ InstrumentName = ReferenceDirectorySqlHelpers.GetString(reader, "InstrumentName"),
+ TypeName = ReferenceDirectorySqlHelpers.GetString(reader, "TypeName"),
+ RangeText = ReferenceDirectorySqlHelpers.GetString(reader, "RangeText"),
+ RegistryNumber = ReferenceDirectorySqlHelpers.GetString(reader, "RegistryNumber"),
+ SerialNumber = ReferenceDirectorySqlHelpers.GetString(reader, "SerialNumber"),
+ InventoryNumber = ReferenceDirectorySqlHelpers.GetString(reader, "InventoryNumber"),
+ LastVerificationOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "LastVerificationOn"),
+ PlanId = ReferenceDirectorySqlHelpers.GetNullableInt32(reader, "PlanId"),
+ EffectiveTemplateId = ReferenceDirectorySqlHelpers.GetNullableInt32(reader, "EffectiveTemplateId"),
+ PeriodMonths = ReferenceDirectorySqlHelpers.GetNullableInt32(reader, "PeriodMonths"),
+ PlannedOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "PlannedOn"),
+ IsExplicitPlan = Convert.ToBoolean(reader.GetValue(reader.GetOrdinal("IsExplicitPlan"))),
+ PlanSource = ReferenceDirectorySqlHelpers.GetString(reader, "PlanSource"),
+ PeriodSource = ReferenceDirectorySqlHelpers.GetString(reader, "PeriodSource")
+ });
+ }
+ }
+ }
+
+ return items;
+ }
+
+ public IReadOnlyList LoadTemplateOptions(int typeSizeId)
+ {
+ if (typeSizeId <= 0)
+ {
+ return Array.Empty();
+ }
+
+ const string sql = @"
+SELECT
+ t.IDTPRMCP AS Id,
+ t.IDTPRZ AS TypeSizeId,
+ t.IDSPVDMC AS CycleId,
+ cycle.NMVDMC AS CycleName,
+ t.IDGRSI AS GroupId,
+ groups.NMGRSI AS GroupName,
+ t.PRMK AS PeriodMonths,
+ CAST(t.KM AS nvarchar(max)) AS Comment
+FROM dbo.TPRMCP t
+LEFT JOIN dbo.SPVDMC cycle ON cycle.IDSPVDMC = t.IDSPVDMC
+LEFT JOIN dbo.GRSI groups ON groups.IDGRSI = t.IDGRSI
+WHERE t.IDTPRZ = @TypeSizeId
+ORDER BY t.PRMK, cycle.NMVDMC, groups.NMGRSI, t.IDTPRMCP;";
+
+ var items = new List();
+
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ using (var command = new SqlCommand(sql, connection))
+ {
+ connection.Open();
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@TypeSizeId", SqlDbType.Int).Value = typeSizeId;
+
+ using (var reader = command.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ items.Add(new PlanningTemplateOption
+ {
+ Id = ReferenceDirectorySqlHelpers.GetInt32(reader, "Id"),
+ TypeSizeId = ReferenceDirectorySqlHelpers.GetInt32(reader, "TypeSizeId"),
+ CycleId = ReferenceDirectorySqlHelpers.GetNullableInt32(reader, "CycleId"),
+ CycleName = ReferenceDirectorySqlHelpers.GetString(reader, "CycleName"),
+ GroupId = ReferenceDirectorySqlHelpers.GetNullableInt32(reader, "GroupId"),
+ GroupName = ReferenceDirectorySqlHelpers.GetString(reader, "GroupName"),
+ PeriodMonths = ReferenceDirectorySqlHelpers.GetInt32(reader, "PeriodMonths"),
+ Comment = ReferenceDirectorySqlHelpers.GetString(reader, "Comment")
+ });
+ }
+ }
+ }
+
+ return items;
+ }
+
+ public void UpdatePlanItem(PlanningEditResult item)
+ {
+ if (item == null || !item.PlanId.HasValue || item.PlanId.Value <= 0)
+ {
+ throw new InvalidOperationException("Не выбрана запись EKZMCP для изменения.");
+ }
+
+ const string sql = @"
+UPDATE dbo.EKZMCP
+SET IDEKZ = @InstrumentId,
+ IDTPRMCP = @TemplateId,
+ DTMKPLO = @PlannedOn
+WHERE IDEKZMCP = @Id;
+
+SELECT @@ROWCOUNT;";
+
+ using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
+ using (var command = new SqlCommand(sql, connection))
+ {
+ connection.Open();
+ EnsurePlanIsUnique(connection, item.InstrumentId, item.TemplateId, item.PlanId.Value);
+
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@Id", SqlDbType.Int).Value = item.PlanId.Value;
+ command.Parameters.Add("@InstrumentId", SqlDbType.Int).Value = item.InstrumentId;
+ command.Parameters.Add("@TemplateId", SqlDbType.Int).Value = item.TemplateId;
+ command.Parameters.Add("@PlannedOn", SqlDbType.DateTime).Value = item.PlannedOn.Date;
+
+ try
+ {
+ if (Convert.ToInt32(command.ExecuteScalar()) == 0)
+ {
+ throw new InvalidOperationException("Запись EKZMCP для изменения не найдена.");
+ }
+ }
+ catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "XAK1EKZMCP"))
+ {
+ throw CreatePlanDuplicateException(ex);
+ }
+ }
+ }
+
+ private static Exception CreatePlanDuplicateException(SqlException ex)
+ {
+ var suffix = ex == null || string.IsNullOrWhiteSpace(ex.Message)
+ ? string.Empty
+ : " " + ex.Message.Trim();
+
+ return new InvalidOperationException("Для выбранного прибора и периода TPRMCP запись EKZMCP уже существует." + suffix, ex);
+ }
+
+ private static void EnsurePlanIsUnique(SqlConnection connection, int instrumentId, int templateId, int? excludeId)
+ {
+ const string sql = @"
+SELECT TOP (1) IDEKZMCP
+FROM dbo.EKZMCP
+WHERE IDEKZ = @InstrumentId
+ AND IDTPRMCP = @TemplateId
+ AND (@ExcludeId IS NULL OR IDEKZMCP <> @ExcludeId);";
+
+ using (var command = new SqlCommand(sql, connection))
+ {
+ command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
+ command.Parameters.Add("@InstrumentId", SqlDbType.Int).Value = instrumentId;
+ command.Parameters.Add("@TemplateId", SqlDbType.Int).Value = templateId;
+ command.Parameters.Add("@ExcludeId", SqlDbType.Int).Value = (object)excludeId ?? DBNull.Value;
+
+ if (command.ExecuteScalar() != null)
+ {
+ throw CreatePlanDuplicateException(null);
+ }
+ }
+ }
+ }
+}
diff --git a/XLAB/PlanningWindow.xaml b/XLAB/PlanningWindow.xaml
new file mode 100644
index 0000000..a565877
--- /dev/null
+++ b/XLAB/PlanningWindow.xaml
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XLAB/PlanningWindow.xaml.cs b/XLAB/PlanningWindow.xaml.cs
new file mode 100644
index 0000000..d81cdc4
--- /dev/null
+++ b/XLAB/PlanningWindow.xaml.cs
@@ -0,0 +1,33 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace XLAB
+{
+ public partial class PlanningWindow : Window
+ {
+ private readonly PlanningWindowViewModel _viewModel;
+
+ public PlanningWindow()
+ {
+ InitializeComponent();
+ _viewModel = new PlanningWindowViewModel(new PlanningService(), new EkzDirectoryService(), new PlanningDialogService(this));
+ DataContext = _viewModel;
+ }
+
+ private void DataGridRow_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ var row = sender as DataGridRow;
+ if (row != null)
+ {
+ row.IsSelected = true;
+ row.Focus();
+ }
+ }
+
+ private async void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ await _viewModel.InitializeAsync();
+ }
+ }
+}
diff --git a/XLAB/PlanningWindowViewModel.cs b/XLAB/PlanningWindowViewModel.cs
new file mode 100644
index 0000000..209862e
--- /dev/null
+++ b/XLAB/PlanningWindowViewModel.cs
@@ -0,0 +1,546 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Input;
+
+namespace XLAB
+{
+ internal sealed class PlanningWindowViewModel : ObservableObject
+ {
+ private readonly IPlanningDialogService _dialogService;
+ private readonly EkzDirectoryService _ekzService;
+ private readonly PlanningService _service;
+ private List _ekzCache;
+ private bool _isBusy;
+ private List _planCache;
+ private string _searchText;
+ private PlanningItem _selectedItem;
+ private int _selectedOwnerFilterId;
+ private int _selectedYear;
+ private string _statusText;
+
+ public PlanningWindowViewModel(PlanningService service, EkzDirectoryService ekzService, IPlanningDialogService dialogService)
+ {
+ _service = service;
+ _ekzService = ekzService;
+ _dialogService = dialogService;
+ _planCache = new List();
+ _ekzCache = new List();
+
+ PlanItems = new ObservableCollection();
+ OwnerFilterItems = new ObservableCollection();
+ YearItems = new ObservableCollection();
+
+ var currentYear = DateTime.Today.Year;
+ for (var year = currentYear; year <= currentYear + 5; year++)
+ {
+ YearItems.Add(year);
+ }
+
+ _selectedYear = currentYear + 1;
+
+ OwnerFilterItems.Add(new DirectoryLookupItem { Id = 0, Name = "Все организации" });
+
+ AddInstrumentCommand = new RelayCommand(delegate { AddInstrumentAsync(); }, delegate { return !IsBusy; });
+ EditInstrumentCommand = new RelayCommand(delegate { EditInstrumentAsync(); }, delegate { return !IsBusy && SelectedItem != null; });
+ AddPlanCommand = new RelayCommand(delegate { AddPlanAsync(); }, delegate { return !IsBusy && _ekzCache.Count > 0; });
+ EditPlanCommand = new RelayCommand(delegate { EditPlanAsync(); }, delegate { return !IsBusy && SelectedItem != null && SelectedItem.CanPersistPlan; });
+ DeletePlanCommand = new RelayCommand(delegate { DeletePlanAsync(); }, delegate { return !IsBusy && SelectedItem != null && SelectedItem.PlanId.HasValue; });
+ RefreshCommand = new RelayCommand(delegate { RefreshAsync(); }, delegate { return !IsBusy; });
+
+ UpdateStatus();
+ }
+
+ public ICommand AddInstrumentCommand { get; private set; }
+
+ public ICommand AddPlanCommand { get; private set; }
+
+ public ICommand DeletePlanCommand { get; private set; }
+
+ public ICommand EditInstrumentCommand { get; private set; }
+
+ public ICommand EditPlanCommand { get; private set; }
+
+ public bool IsBusy
+ {
+ get { return _isBusy; }
+ private set
+ {
+ if (SetProperty(ref _isBusy, value))
+ {
+ RaiseCommandStates();
+ }
+ }
+ }
+
+ public ObservableCollection OwnerFilterItems { get; private set; }
+
+ public ObservableCollection PlanItems { get; private set; }
+
+ public ICommand RefreshCommand { get; private set; }
+
+ public string SearchText
+ {
+ get { return _searchText; }
+ set
+ {
+ if (SetProperty(ref _searchText, value))
+ {
+ ApplyFilter(GetPreferredRowKey(SelectedItem));
+ }
+ }
+ }
+
+ public PlanningItem SelectedItem
+ {
+ get { return _selectedItem; }
+ set
+ {
+ if (SetProperty(ref _selectedItem, value))
+ {
+ RaiseCommandStates();
+ UpdateStatus();
+ }
+ }
+ }
+
+ public int SelectedOwnerFilterId
+ {
+ get { return _selectedOwnerFilterId; }
+ set
+ {
+ if (SetProperty(ref _selectedOwnerFilterId, value))
+ {
+ ApplyFilter(GetPreferredRowKey(SelectedItem));
+ }
+ }
+ }
+
+ public int SelectedYear
+ {
+ get { return _selectedYear; }
+ set
+ {
+ if (SetProperty(ref _selectedYear, value) && !IsBusy)
+ {
+ RefreshAsync();
+ }
+ }
+ }
+
+ public string StatusText
+ {
+ get { return _statusText; }
+ private set { SetProperty(ref _statusText, value); }
+ }
+
+ public ObservableCollection YearItems { get; private set; }
+
+ public async Task InitializeAsync()
+ {
+ await ExecuteBusyOperationAsync(delegate { return RefreshCoreAsync(null); });
+ }
+
+ private async void AddInstrumentAsync()
+ {
+ var result = _dialogService.ShowEkzEditDialog(new EkzDirectoryItem(), true, _ekzCache.ToList(), _ekzService);
+ if (result == null)
+ {
+ return;
+ }
+
+ await RunMutationOperation(async delegate
+ {
+ var createdId = await Task.Run(delegate { return _ekzService.AddEkzItem(result); });
+ await RefreshCoreAsync(CreatePreferredRowKey(createdId, null));
+ _dialogService.ShowInfo("Запись EKZ добавлена.");
+ });
+ }
+
+ private async void AddPlanAsync()
+ {
+ if (_ekzCache.Count == 0)
+ {
+ _dialogService.ShowWarning("Список приборов EKZ пуст.");
+ return;
+ }
+
+ var seed = new PlanningEditSeed
+ {
+ TargetYear = SelectedYear,
+ InstrumentId = SelectedItem == null ? 0 : SelectedItem.InstrumentId,
+ TemplateId = SelectedItem == null ? (int?)null : SelectedItem.EffectiveTemplateId,
+ PlannedOn = SelectedItem != null && SelectedItem.PlannedOn.HasValue && SelectedItem.PlannedOn.Value.Year == SelectedYear
+ ? SelectedItem.PlannedOn
+ : (DateTime?)null
+ };
+
+ var result = _dialogService.ShowPlanningEditDialog(seed, true, BuildInstrumentOptions(), _service);
+ if (result == null)
+ {
+ return;
+ }
+
+ await RunMutationOperation(async delegate
+ {
+ var createdId = await Task.Run(delegate { return _service.AddPlanItem(result); });
+ await RefreshCoreAsync(CreatePreferredRowKey(result.InstrumentId, createdId));
+ _dialogService.ShowInfo("Плановая запись EKZMCP добавлена.");
+ });
+ }
+
+ private void ApplyFilter(string preferredKey)
+ {
+ var filteredItems = _planCache.Where(delegate(PlanningItem item)
+ {
+ return MatchesOwnerFilter(item) && MatchesSearch(item);
+ }).ToList();
+
+ PlanItems.Clear();
+ foreach (var item in filteredItems)
+ {
+ PlanItems.Add(item);
+ }
+
+ SelectedItem = string.IsNullOrWhiteSpace(preferredKey)
+ ? PlanItems.FirstOrDefault()
+ : PlanItems.FirstOrDefault(delegate(PlanningItem item) { return string.Equals(GetPreferredRowKey(item), preferredKey, StringComparison.OrdinalIgnoreCase); })
+ ?? PlanItems.FirstOrDefault();
+
+ UpdateStatus();
+ }
+
+ private List BuildInstrumentOptions()
+ {
+ return _ekzCache
+ .Select(delegate(EkzDirectoryItem item)
+ {
+ var tail = string.IsNullOrWhiteSpace(item.InventoryNumber)
+ ? string.Empty
+ : string.Format(" / инв.№ {0}", item.InventoryNumber);
+
+ return new PlanningInstrumentOption
+ {
+ Id = item.Id,
+ TypeSizeId = item.TypeSizeId,
+ OwnerOrganizationId = item.OwnerOrganizationId,
+ DisplayName = string.Format(
+ "{0} / {1} / {2} / {3} / {4} / зав.№ {5}{6}",
+ item.OwnerOrganizationName,
+ item.MeasurementAreaName,
+ item.InstrumentName,
+ item.TypeName,
+ item.RangeText,
+ item.SerialNumber,
+ tail)
+ };
+ })
+ .OrderBy(delegate(PlanningInstrumentOption item) { return item.DisplayName; }, StringComparer.CurrentCultureIgnoreCase)
+ .ToList();
+ }
+
+ private static string CloneValue(string value)
+ {
+ return value == null ? string.Empty : string.Copy(value);
+ }
+
+ private static EkzDirectoryItem CloneEkz(EkzDirectoryItem source)
+ {
+ return new EkzDirectoryItem
+ {
+ Id = source.Id,
+ TypeSizeId = source.TypeSizeId,
+ MeasurementAreaName = CloneValue(source.MeasurementAreaName),
+ InstrumentName = CloneValue(source.InstrumentName),
+ TypeName = CloneValue(source.TypeName),
+ RangeText = CloneValue(source.RangeText),
+ AccuracyText = CloneValue(source.AccuracyText),
+ RegistryNumber = CloneValue(source.RegistryNumber),
+ OwnerOrganizationId = source.OwnerOrganizationId,
+ OwnerOrganizationName = CloneValue(source.OwnerOrganizationName),
+ SerialNumber = CloneValue(source.SerialNumber),
+ InventoryNumber = CloneValue(source.InventoryNumber),
+ Notes = CloneValue(source.Notes)
+ };
+ }
+
+ private static string CreatePreferredRowKey(int instrumentId, int? planId)
+ {
+ return string.Format(
+ "{0}:{1}",
+ instrumentId,
+ planId.HasValue ? planId.Value.ToString() : "0");
+ }
+
+ private async void DeletePlanAsync()
+ {
+ if (SelectedItem == null || !SelectedItem.PlanId.HasValue)
+ {
+ return;
+ }
+
+ var selected = SelectedItem;
+ var plannedDateText = selected.PlannedOn.HasValue
+ ? selected.PlannedOn.Value.ToString("d")
+ : "без даты";
+ var question = string.Format(
+ "Удалить плановую запись EKZMCP для прибора \"{0}\" с датой {1}?",
+ selected.SerialNumber,
+ plannedDateText);
+
+ if (!_dialogService.Confirm(question))
+ {
+ return;
+ }
+
+ await RunMutationOperation(async delegate
+ {
+ await Task.Run(delegate { _service.DeletePlanItem(selected.PlanId.Value); });
+ await RefreshCoreAsync(CreatePreferredRowKey(selected.InstrumentId, null));
+ _dialogService.ShowInfo("Плановая запись EKZMCP удалена.");
+ });
+ }
+
+ private async void EditInstrumentAsync()
+ {
+ if (SelectedItem == null)
+ {
+ return;
+ }
+
+ var current = _ekzCache.FirstOrDefault(delegate(EkzDirectoryItem item) { return item.Id == SelectedItem.InstrumentId; });
+ if (current == null)
+ {
+ _dialogService.ShowWarning("Выбранный прибор не найден в EKZ.");
+ return;
+ }
+
+ var result = _dialogService.ShowEkzEditDialog(CloneEkz(current), false, _ekzCache.ToList(), _ekzService);
+ if (result == null)
+ {
+ return;
+ }
+
+ var preferredKey = GetPreferredRowKey(SelectedItem);
+
+ await RunMutationOperation(async delegate
+ {
+ await Task.Run(delegate { _ekzService.UpdateEkzItem(result); });
+ await RefreshCoreAsync(preferredKey);
+ _dialogService.ShowInfo("Запись EKZ обновлена.");
+ });
+ }
+
+ private async void EditPlanAsync()
+ {
+ if (SelectedItem == null)
+ {
+ return;
+ }
+
+ if (!SelectedItem.CanPersistPlan)
+ {
+ _dialogService.ShowWarning("Для выбранного прибора найден только расчетный период без TPRMCP. Сохранить запись в EKZMCP нельзя.");
+ return;
+ }
+
+ var seed = new PlanningEditSeed
+ {
+ TargetYear = SelectedYear,
+ PlanId = SelectedItem.PlanId,
+ InstrumentId = SelectedItem.InstrumentId,
+ TemplateId = SelectedItem.EffectiveTemplateId,
+ PlannedOn = SelectedItem.PlannedOn
+ };
+
+ var isNew = !SelectedItem.PlanId.HasValue;
+ var result = _dialogService.ShowPlanningEditDialog(seed, isNew, BuildInstrumentOptions(), _service);
+ if (result == null)
+ {
+ return;
+ }
+
+ await RunMutationOperation(async delegate
+ {
+ if (result.PlanId.HasValue)
+ {
+ await Task.Run(delegate { _service.UpdatePlanItem(result); });
+ await RefreshCoreAsync(CreatePreferredRowKey(result.InstrumentId, result.PlanId));
+ _dialogService.ShowInfo("Плановая запись EKZMCP обновлена.");
+ }
+ else
+ {
+ var createdId = await Task.Run(delegate { return _service.AddPlanItem(result); });
+ await RefreshCoreAsync(CreatePreferredRowKey(result.InstrumentId, createdId));
+ _dialogService.ShowInfo("Плановая запись EKZMCP добавлена.");
+ }
+ });
+ }
+
+ private async Task ExecuteBusyOperationAsync(Func operation)
+ {
+ try
+ {
+ IsBusy = true;
+ await operation();
+ }
+ catch (InvalidOperationException ex)
+ {
+ _dialogService.ShowWarning(ex.Message);
+ }
+ catch (Exception ex)
+ {
+ _dialogService.ShowError(ex.Message);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private string[] GetSearchTokens()
+ {
+ return (SearchText ?? string.Empty)
+ .Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(delegate(string token) { return token.Trim().ToUpperInvariant(); })
+ .Where(delegate(string token) { return token.Length > 0; })
+ .ToArray();
+ }
+
+ private static string GetPreferredRowKey(PlanningItem item)
+ {
+ return item == null ? null : CreatePreferredRowKey(item.InstrumentId, item.PlanId);
+ }
+
+ private bool MatchesOwnerFilter(PlanningItem item)
+ {
+ return SelectedOwnerFilterId <= 0
+ || (item != null && item.OwnerOrganizationId == SelectedOwnerFilterId);
+ }
+
+ private bool MatchesSearch(PlanningItem item)
+ {
+ var tokens = GetSearchTokens();
+ if (tokens.Length == 0)
+ {
+ return true;
+ }
+
+ var haystack = string.Join(
+ " ",
+ new[]
+ {
+ item == null ? null : item.OwnerOrganizationName,
+ item == null ? null : item.MeasurementAreaName,
+ item == null ? null : item.InstrumentName,
+ item == null ? null : item.TypeName,
+ item == null ? null : item.RangeText,
+ item == null ? null : item.RegistryNumber,
+ item == null ? null : item.SerialNumber,
+ item == null ? null : item.InventoryNumber,
+ item == null ? null : item.PlanSource,
+ item == null ? null : item.PeriodSource
+ })
+ .ToUpperInvariant();
+
+ return tokens.All(delegate(string token) { return haystack.Contains(token); });
+ }
+
+ private void RaiseCommandStates()
+ {
+ var addInstrumentCommand = AddInstrumentCommand as RelayCommand;
+ if (addInstrumentCommand != null)
+ {
+ addInstrumentCommand.RaiseCanExecuteChanged();
+ }
+
+ var editInstrumentCommand = EditInstrumentCommand as RelayCommand;
+ if (editInstrumentCommand != null)
+ {
+ editInstrumentCommand.RaiseCanExecuteChanged();
+ }
+
+ var addPlanCommand = AddPlanCommand as RelayCommand;
+ if (addPlanCommand != null)
+ {
+ addPlanCommand.RaiseCanExecuteChanged();
+ }
+
+ var editPlanCommand = EditPlanCommand as RelayCommand;
+ if (editPlanCommand != null)
+ {
+ editPlanCommand.RaiseCanExecuteChanged();
+ }
+
+ var deletePlanCommand = DeletePlanCommand as RelayCommand;
+ if (deletePlanCommand != null)
+ {
+ deletePlanCommand.RaiseCanExecuteChanged();
+ }
+
+ var refreshCommand = RefreshCommand as RelayCommand;
+ if (refreshCommand != null)
+ {
+ refreshCommand.RaiseCanExecuteChanged();
+ }
+ }
+
+ private async Task RefreshCoreAsync(string preferredKey)
+ {
+ var planTask = Task.Run(delegate { return _service.LoadPlanItems(SelectedYear); });
+ var ownerTask = Task.Run(delegate { return _service.LoadOwnerItems(); });
+ var ekzTask = Task.Run(delegate { return _ekzService.LoadEkzItems(); });
+
+ await Task.WhenAll(planTask, ownerTask, ekzTask);
+
+ _planCache = planTask.Result.ToList();
+ _ekzCache = ekzTask.Result.ToList();
+
+ ApplyOwnerItems(ownerTask.Result);
+ ApplyFilter(preferredKey);
+ }
+
+ private void ApplyOwnerItems(IReadOnlyList items)
+ {
+ OwnerFilterItems.Clear();
+ OwnerFilterItems.Add(new DirectoryLookupItem { Id = 0, Name = "Все организации" });
+
+ foreach (var item in items ?? Array.Empty())
+ {
+ OwnerFilterItems.Add(item);
+ }
+
+ if (!OwnerFilterItems.Any(delegate(DirectoryLookupItem item) { return item.Id == SelectedOwnerFilterId; }))
+ {
+ SelectedOwnerFilterId = 0;
+ }
+ }
+
+ private async void RefreshAsync()
+ {
+ await ExecuteBusyOperationAsync(delegate { return RefreshCoreAsync(GetPreferredRowKey(SelectedItem)); });
+ }
+
+ private async Task RunMutationOperation(Func operation)
+ {
+ await ExecuteBusyOperationAsync(operation);
+ }
+
+ private void UpdateStatus()
+ {
+ var explicitCount = PlanItems.Count(delegate(PlanningItem item) { return item.IsExplicitPlan; });
+ var calculatedCount = PlanItems.Count - explicitCount;
+ var nonPersistableCount = PlanItems.Count(delegate(PlanningItem item) { return !item.CanPersistPlan; });
+
+ StatusText = string.Format(
+ "Год: {0}. Позиций: {1}. Явных планов EKZMCP: {2}. Расчетных позиций: {3}. Без TPRMCP: {4}.",
+ SelectedYear,
+ PlanItems.Count,
+ explicitCount,
+ calculatedCount,
+ nonPersistableCount);
+ }
+ }
+}
diff --git a/XLAB/XLAB.csproj b/XLAB/XLAB.csproj
index c204b95..b6bb1eb 100644
--- a/XLAB/XLAB.csproj
+++ b/XLAB/XLAB.csproj
@@ -112,6 +112,9 @@
+
+
+
@@ -284,6 +287,24 @@
Code
+
+ MSBuild:Compile
+ Designer
+
+
+ PlanningEditWindow.xaml
+ Code
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+ PlanningWindow.xaml
+ Code
+
+
MSBuild:Compile
Designer