From 5995d4fa72a63ed1dbb40ba64be1cef8444c4e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Sat, 14 Mar 2026 21:20:14 +0300 Subject: [PATCH] edit --- XLAB/DialogService.cs | 12 + XLAB/MainWindow.xaml | 4 + XLAB/MainWindow.xaml.cs | 14 + XLAB/PsvDataService.cs | 388 +++++ XLAB/PsvModels.cs | 23 + XLAB/SpoiDirectoryWindow.xaml | 86 + XLAB/SpoiDirectoryWindow.xaml.cs | 21 + XLAB/SpoiDirectoryWindowViewModel.cs | 294 ++++ XLAB/SpoiEditWindow.xaml | 65 + XLAB/SpoiEditWindow.xaml.cs | 20 + XLAB/SpoiEditWindowViewModel.cs | 148 ++ XLAB/TipsEditWindow.xaml | 87 + XLAB/TipsEditWindow.xaml.cs | 20 + XLAB/TipsEditWindowViewModel.cs | 239 +++ XLAB/TprmcpEditWindow.xaml | 48 + XLAB/TprmcpEditWindow.xaml.cs | 20 + XLAB/TprmcpEditWindowViewModel.cs | 99 ++ XLAB/TprmkEditWindow.xaml | 78 + XLAB/TprmkEditWindow.xaml.cs | 20 + XLAB/TprmkEditWindowViewModel.cs | 153 ++ XLAB/TprzEditWindow.xaml | 52 + XLAB/TprzEditWindow.xaml.cs | 20 + XLAB/TprzEditWindowViewModel.cs | 92 ++ XLAB/TpvdklEditWindow.xaml | 40 + XLAB/TpvdklEditWindow.xaml.cs | 20 + XLAB/TpvdklEditWindowViewModel.cs | 66 + XLAB/TpvdklWindow.xaml | 68 + XLAB/TpvdklWindow.xaml.cs | 33 + XLAB/TpvdklWindowViewModel.cs | 145 ++ XLAB/TypeSizeDialogService.cs | 115 ++ XLAB/TypeSizeDirectoryModels.cs | 191 +++ XLAB/TypeSizeDirectoryService.cs | 1889 ++++++++++++++++++++++ XLAB/TypeSizeDirectoryWindow.xaml | 199 +++ XLAB/TypeSizeDirectoryWindow.xaml.cs | 33 + XLAB/TypeSizeDirectoryWindowViewModel.cs | 664 ++++++++ XLAB/XLAB.csproj | 84 + 36 files changed, 5550 insertions(+) create mode 100644 XLAB/SpoiDirectoryWindow.xaml create mode 100644 XLAB/SpoiDirectoryWindow.xaml.cs create mode 100644 XLAB/SpoiDirectoryWindowViewModel.cs create mode 100644 XLAB/SpoiEditWindow.xaml create mode 100644 XLAB/SpoiEditWindow.xaml.cs create mode 100644 XLAB/SpoiEditWindowViewModel.cs create mode 100644 XLAB/TipsEditWindow.xaml create mode 100644 XLAB/TipsEditWindow.xaml.cs create mode 100644 XLAB/TipsEditWindowViewModel.cs create mode 100644 XLAB/TprmcpEditWindow.xaml create mode 100644 XLAB/TprmcpEditWindow.xaml.cs create mode 100644 XLAB/TprmcpEditWindowViewModel.cs create mode 100644 XLAB/TprmkEditWindow.xaml create mode 100644 XLAB/TprmkEditWindow.xaml.cs create mode 100644 XLAB/TprmkEditWindowViewModel.cs create mode 100644 XLAB/TprzEditWindow.xaml create mode 100644 XLAB/TprzEditWindow.xaml.cs create mode 100644 XLAB/TprzEditWindowViewModel.cs create mode 100644 XLAB/TpvdklEditWindow.xaml create mode 100644 XLAB/TpvdklEditWindow.xaml.cs create mode 100644 XLAB/TpvdklEditWindowViewModel.cs create mode 100644 XLAB/TpvdklWindow.xaml create mode 100644 XLAB/TpvdklWindow.xaml.cs create mode 100644 XLAB/TpvdklWindowViewModel.cs create mode 100644 XLAB/TypeSizeDialogService.cs create mode 100644 XLAB/TypeSizeDirectoryModels.cs create mode 100644 XLAB/TypeSizeDirectoryService.cs create mode 100644 XLAB/TypeSizeDirectoryWindow.xaml create mode 100644 XLAB/TypeSizeDirectoryWindow.xaml.cs create mode 100644 XLAB/TypeSizeDirectoryWindowViewModel.cs diff --git a/XLAB/DialogService.cs b/XLAB/DialogService.cs index 58d7027..3d2744a 100644 --- a/XLAB/DialogService.cs +++ b/XLAB/DialogService.cs @@ -13,6 +13,8 @@ namespace XLAB IReadOnlyList ShowCloneVerificationDialog(CloneVerificationSeed seed); + SpoiDirectoryItem ShowSpoiEditDialog(SpoiDirectoryItem seed, bool isNew, IReadOnlyList existingItems); + SpnmtpDirectoryItem ShowSpnmtpEditDialog(SpnmtpDirectoryItem seed, bool isNew, IReadOnlyList existingItems); VerificationEditResult ShowVerificationDialog( @@ -78,6 +80,16 @@ namespace XLAB return result.HasValue && result.Value ? viewModel.GetSerialNumbers() : null; } + public SpoiDirectoryItem ShowSpoiEditDialog(SpoiDirectoryItem seed, bool isNew, IReadOnlyList existingItems) + { + var viewModel = new SpoiEditWindowViewModel(seed, isNew, existingItems); + var window = new SpoiEditWindow(viewModel); + window.Owner = _owner; + + var result = window.ShowDialog(); + return result.HasValue && result.Value ? viewModel.ToResult() : null; + } + public SpnmtpDirectoryItem ShowSpnmtpEditDialog(SpnmtpDirectoryItem seed, bool isNew, IReadOnlyList existingItems) { var viewModel = new SpnmtpEditWindowViewModel(seed, isNew, existingItems); diff --git a/XLAB/MainWindow.xaml b/XLAB/MainWindow.xaml index b55d865..c586ed4 100644 --- a/XLAB/MainWindow.xaml +++ b/XLAB/MainWindow.xaml @@ -17,6 +17,10 @@ + + diff --git a/XLAB/MainWindow.xaml.cs b/XLAB/MainWindow.xaml.cs index 13c53af..5f12a75 100644 --- a/XLAB/MainWindow.xaml.cs +++ b/XLAB/MainWindow.xaml.cs @@ -52,6 +52,20 @@ namespace XLAB window.ShowDialog(); } + private void TypeSizeDirectoryMenuItem_Click(object sender, RoutedEventArgs e) + { + var window = new TypeSizeDirectoryWindow(); + window.Owner = this; + window.ShowDialog(); + } + + private void SpoiDirectoryMenuItem_Click(object sender, RoutedEventArgs e) + { + var window = new SpoiDirectoryWindow(); + window.Owner = this; + window.ShowDialog(); + } + private async void Window_Loaded(object sender, RoutedEventArgs e) { await _viewModel.InitializeAsync(); diff --git a/XLAB/PsvDataService.cs b/XLAB/PsvDataService.cs index 004081d..cf789da 100644 --- a/XLAB/PsvDataService.cs +++ b/XLAB/PsvDataService.cs @@ -146,6 +146,184 @@ ORDER BY fr.NMFRD, v.IDVDODVDD;"; return forms; } + public IReadOnlyList LoadSpoiItems() + { + const string sql = @" +SELECT + s.IDSPOI AS Id, + s.KDOI AS Code, + s.NMOI AS Name +FROM dbo.SPOI s +ORDER BY s.NMOI, s.KDOI, s.IDSPOI;"; + + var items = new List(); + + using (var connection = CreateConnection()) + using (var command = new SqlCommand(sql, connection)) + { + connection.Open(); + command.CommandTimeout = 60; + + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + items.Add(new SpoiDirectoryItem + { + Id = GetInt32(reader, "Id"), + Code = GetString(reader, "Code"), + Name = GetString(reader, "Name") + }); + } + } + } + + return items; + } + + public int AddSpoiItem(SpoiDirectoryItem item) + { + var normalizedItem = NormalizeSpoiItem(item); + + const string sql = @" +INSERT INTO dbo.SPOI +( + KDOI, + NMOI +) +VALUES +( + @Code, + @Name +); + +SELECT CAST(SCOPE_IDENTITY() AS int);"; + + using (var connection = CreateConnection()) + using (var command = new SqlCommand(sql, connection)) + { + connection.Open(); + EnsureSpoiCodeIsUnique(connection, normalizedItem.Code, null); + EnsureSpoiNameIsUnique(connection, normalizedItem.Name, null); + command.CommandTimeout = 60; + command.Parameters.Add("@Code", SqlDbType.VarChar, SpoiDirectoryRules.CodeMaxLength).Value = normalizedItem.Code; + command.Parameters.Add("@Name", SqlDbType.VarChar, SpoiDirectoryRules.NameMaxLength).Value = normalizedItem.Name; + + try + { + return Convert.ToInt32(command.ExecuteScalar()); + } + catch (SqlException ex) when (IsSpoiDuplicateCodeViolation(ex)) + { + throw CreateSpoiDuplicateCodeException(normalizedItem.Code, ex); + } + catch (SqlException ex) when (IsSpoiDuplicateNameViolation(ex)) + { + throw CreateSpoiDuplicateNameException(normalizedItem.Name, ex); + } + } + } + + public void UpdateSpoiItem(SpoiDirectoryItem item) + { + var normalizedItem = NormalizeSpoiItem(item); + if (normalizedItem.Id <= 0) + { + throw new InvalidOperationException("Не выбрана запись SPOI для изменения."); + } + + const string sql = @" +UPDATE dbo.SPOI +SET KDOI = @Code, + NMOI = @Name +WHERE IDSPOI = @Id; + +SELECT @@ROWCOUNT;"; + + using (var connection = CreateConnection()) + using (var command = new SqlCommand(sql, connection)) + { + connection.Open(); + EnsureSpoiCodeIsUnique(connection, normalizedItem.Code, normalizedItem.Id); + EnsureSpoiNameIsUnique(connection, normalizedItem.Name, normalizedItem.Id); + command.CommandTimeout = 60; + command.Parameters.Add("@Id", SqlDbType.Int).Value = normalizedItem.Id; + command.Parameters.Add("@Code", SqlDbType.VarChar, SpoiDirectoryRules.CodeMaxLength).Value = normalizedItem.Code; + command.Parameters.Add("@Name", SqlDbType.VarChar, SpoiDirectoryRules.NameMaxLength).Value = normalizedItem.Name; + + try + { + if (Convert.ToInt32(command.ExecuteScalar()) == 0) + { + throw new InvalidOperationException("Запись SPOI для изменения не найдена."); + } + } + catch (SqlException ex) when (IsSpoiDuplicateCodeViolation(ex)) + { + throw CreateSpoiDuplicateCodeException(normalizedItem.Code, ex); + } + catch (SqlException ex) when (IsSpoiDuplicateNameViolation(ex)) + { + throw CreateSpoiDuplicateNameException(normalizedItem.Name, ex); + } + } + } + + public SpoiDeleteResult DeleteSpoiItem(int id) + { + if (id <= 0) + { + throw new InvalidOperationException("Не выбрана запись SPOI для удаления."); + } + + const string sql = @" +DELETE FROM dbo.SPOI +WHERE IDSPOI = @Id; + +SELECT @@ROWCOUNT;"; + + try + { + using (var connection = CreateConnection()) + { + connection.Open(); + var blockers = LoadSpoiDeleteBlockers(connection, id); + if (blockers.Count > 0) + { + return new SpoiDeleteResult + { + IsDeleted = false, + WarningMessage = CreateSpoiDeleteBlockedMessage(blockers) + }; + } + + using (var command = new SqlCommand(sql, connection)) + { + command.CommandTimeout = 60; + command.Parameters.Add("@Id", SqlDbType.Int).Value = id; + + if (Convert.ToInt32(command.ExecuteScalar()) == 0) + { + throw new InvalidOperationException("Запись SPOI для удаления не найдена."); + } + } + + return new SpoiDeleteResult + { + IsDeleted = true + }; + } + } + catch (SqlException ex) when (ex.Number == 547) + { + return new SpoiDeleteResult + { + IsDeleted = false, + WarningMessage = CreateSpoiDeleteBlockedMessage(ex) + }; + } + } + public IReadOnlyList LoadSpnmtpItems() { const string sql = @" @@ -2304,6 +2482,216 @@ WHERE z.IDEKZ = @InstrumentId return value.Trim(); } + private static SpoiDirectoryItem NormalizeSpoiItem(SpoiDirectoryItem item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + var code = NormalizeRequiredSpoiValue( + item.Code, + "Код ОИ не заполнен.", + SpoiDirectoryRules.CodeMaxLength, + "Код ОИ превышает допустимую длину."); + var name = NormalizeRequiredSpoiValue( + item.Name, + "Область измерений не заполнена.", + SpoiDirectoryRules.NameMaxLength, + "Область измерений превышает допустимую длину."); + + return new SpoiDirectoryItem + { + Id = item.Id, + Code = code, + Name = name + }; + } + + private static void EnsureSpoiCodeIsUnique(SqlConnection connection, string code, int? excludeId) + { + const string sql = @" +SELECT TOP (1) IDSPOI +FROM dbo.SPOI +WHERE KDOI = @Code + AND (@ExcludeId IS NULL OR IDSPOI <> @ExcludeId);"; + + using (var command = new SqlCommand(sql, connection)) + { + command.CommandTimeout = 60; + command.Parameters.Add("@Code", SqlDbType.VarChar, SpoiDirectoryRules.CodeMaxLength).Value = code; + command.Parameters.Add("@ExcludeId", SqlDbType.Int).Value = (object)excludeId ?? DBNull.Value; + + var existingId = command.ExecuteScalar(); + if (existingId != null && existingId != DBNull.Value) + { + throw CreateSpoiDuplicateCodeException(code, null); + } + } + } + + private static void EnsureSpoiNameIsUnique(SqlConnection connection, string name, int? excludeId) + { + const string sql = @" +SELECT TOP (1) IDSPOI +FROM dbo.SPOI +WHERE NMOI = @Name + AND (@ExcludeId IS NULL OR IDSPOI <> @ExcludeId);"; + + using (var command = new SqlCommand(sql, connection)) + { + command.CommandTimeout = 60; + command.Parameters.Add("@Name", SqlDbType.VarChar, SpoiDirectoryRules.NameMaxLength).Value = name; + command.Parameters.Add("@ExcludeId", SqlDbType.Int).Value = (object)excludeId ?? DBNull.Value; + + var existingId = command.ExecuteScalar(); + if (existingId != null && existingId != DBNull.Value) + { + throw CreateSpoiDuplicateNameException(name, null); + } + } + } + + private static InvalidOperationException CreateSpoiDuplicateCodeException(string code, Exception innerException) + { + return new InvalidOperationException( + string.Format("Код ОИ \"{0}\" уже существует в справочнике.", code), + innerException); + } + + private static InvalidOperationException CreateSpoiDuplicateNameException(string name, Exception innerException) + { + return new InvalidOperationException( + string.Format("Область измерений \"{0}\" уже существует в справочнике.", name), + innerException); + } + + private static List LoadSpoiDeleteBlockers(SqlConnection connection, int id) + { + const string sql = @" +SELECT blocker.TableName, blocker.LinkCount +FROM +( + SELECT N'FRATGR' AS TableName, COUNT(*) AS LinkCount + FROM dbo.FRATGR + WHERE IDSPOI = @Id + + UNION ALL + + SELECT N'PRATGR', COUNT(*) + FROM dbo.PRATGR + WHERE IDSPOI = @Id + + UNION ALL + + SELECT N'PRSTOI', COUNT(*) + FROM dbo.PRSTOI + WHERE IDSPOI = @Id + + UNION ALL + + SELECT N'TIPS', COUNT(*) + FROM dbo.TIPS + WHERE IDSPOI = @Id +) blocker +WHERE blocker.LinkCount > 0 +ORDER BY blocker.TableName;"; + + var blockers = new List(); + + using (var command = new SqlCommand(sql, connection)) + { + command.CommandTimeout = 60; + command.Parameters.Add("@Id", SqlDbType.Int).Value = id; + + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + blockers.Add(new DeleteBlockerInfo + { + TableName = GetString(reader, "TableName"), + RowCount = GetInt32(reader, "LinkCount") + }); + } + } + } + + return blockers; + } + + private static string CreateSpoiDeleteBlockedMessage(IEnumerable blockers) + { + var blockerList = blockers == null + ? new List() + : blockers.Where(delegate(DeleteBlockerInfo blocker) { return blocker != null; }).ToList(); + var details = blockerList.Count == 0 + ? "FRATGR, PRATGR, PRSTOI, TIPS" + : string.Join(", ", blockerList.Select(delegate(DeleteBlockerInfo blocker) + { + return string.Format("{0}: {1}", blocker.TableName, blocker.RowCount); + })); + + return string.Format( + "Запись SPOI не может быть удалена, потому что на неё есть ссылки в таблицах: {0}. Подтверждённые ограничения БД: FK_FRATGR_SPOI, FK_PRATGR_SPOI, FK_PRSTOI_SPOI, FK_TIPS_SPOI.", + details); + } + + private static string CreateSpoiDeleteBlockedMessage(SqlException ex) + { + if (ex != null && ex.Message.IndexOf("FK_FRATGR_SPOI", StringComparison.OrdinalIgnoreCase) >= 0) + { + return "Запись SPOI не может быть удалена, потому что на неё есть ссылки в таблице FRATGR. Ограничение БД: FK_FRATGR_SPOI (dbo.FRATGR.IDSPOI -> dbo.SPOI.IDSPOI)."; + } + + if (ex != null && ex.Message.IndexOf("FK_PRATGR_SPOI", StringComparison.OrdinalIgnoreCase) >= 0) + { + return "Запись SPOI не может быть удалена, потому что на неё есть ссылки в таблице PRATGR. Ограничение БД: FK_PRATGR_SPOI (dbo.PRATGR.IDSPOI -> dbo.SPOI.IDSPOI)."; + } + + if (ex != null && ex.Message.IndexOf("FK_PRSTOI_SPOI", StringComparison.OrdinalIgnoreCase) >= 0) + { + return "Запись SPOI не может быть удалена, потому что на неё есть ссылки в таблице PRSTOI. Ограничение БД: FK_PRSTOI_SPOI (dbo.PRSTOI.IDSPOI -> dbo.SPOI.IDSPOI)."; + } + + if (ex != null && ex.Message.IndexOf("FK_TIPS_SPOI", StringComparison.OrdinalIgnoreCase) >= 0) + { + return "Запись SPOI не может быть удалена, потому что на неё есть ссылки в таблице TIPS. Ограничение БД: FK_TIPS_SPOI (dbo.TIPS.IDSPOI -> dbo.SPOI.IDSPOI)."; + } + + return "Запись SPOI не может быть удалена из-за ограничения ссылочной целостности в БД."; + } + + private static bool IsSpoiDuplicateCodeViolation(SqlException ex) + { + return ex != null + && (ex.Number == 2601 || ex.Number == 2627) + && ex.Message.IndexOf("XAK2SPOI", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static bool IsSpoiDuplicateNameViolation(SqlException ex) + { + return ex != null + && (ex.Number == 2601 || ex.Number == 2627) + && ex.Message.IndexOf("XAK1SPOI", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static string NormalizeRequiredSpoiValue(string value, string emptyErrorMessage, int maxLength, string tooLongErrorMessage) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidOperationException(emptyErrorMessage); + } + + var normalizedValue = value.Trim(); + if (normalizedValue.Length > maxLength) + { + throw new InvalidOperationException(tooLongErrorMessage); + } + + return normalizedValue; + } + private static SpnmtpDirectoryItem NormalizeSpnmtpItem(SpnmtpDirectoryItem item) { if (item == null) diff --git a/XLAB/PsvModels.cs b/XLAB/PsvModels.cs index 90800cd..7761514 100644 --- a/XLAB/PsvModels.cs +++ b/XLAB/PsvModels.cs @@ -360,6 +360,29 @@ namespace XLAB public string WarningMessage { get; set; } } + public sealed class SpoiDirectoryItem + { + public int Id { get; set; } + + public string Code { get; set; } + + public string Name { get; set; } + } + + internal sealed class SpoiDeleteResult + { + public bool IsDeleted { get; set; } + + public string WarningMessage { get; set; } + } + + internal static class SpoiDirectoryRules + { + public const int CodeMaxLength = 3; + + public const int NameMaxLength = 80; + } + internal static class SpnmtpDirectoryRules { public const int NameMaxLength = 150; diff --git a/XLAB/SpoiDirectoryWindow.xaml b/XLAB/SpoiDirectoryWindow.xaml new file mode 100644 index 0000000..f6eb22f --- /dev/null +++ b/XLAB/SpoiDirectoryWindow.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + +