diff --git a/XLAB/FrpdDirectoryDialogService.cs b/XLAB/FrpdDirectoryDialogService.cs new file mode 100644 index 0000000..b3b3d27 --- /dev/null +++ b/XLAB/FrpdDirectoryDialogService.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Windows; + +namespace XLAB +{ + internal interface IFrpdDirectoryDialogService + { + bool Confirm(string message); + + FrpdDirectoryItem ShowFrpdEditDialog(FrpdDirectoryItem seed, bool isNew, IReadOnlyList existingItems, FrpdDirectoryService service); + + FrpdvdDirectoryItem ShowFrpdvdEditDialog(FrpdvdDirectoryItem seed, bool isNew, IReadOnlyList existingItems, FrpdDirectoryService service); + + void ShowError(string message); + + void ShowInfo(string message); + + void ShowWarning(string message); + } + + internal sealed class FrpdDirectoryDialogService : IFrpdDirectoryDialogService + { + private readonly Window _owner; + + public FrpdDirectoryDialogService(Window owner) + { + _owner = owner; + } + + public bool Confirm(string message) + { + return MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes; + } + + public FrpdDirectoryItem ShowFrpdEditDialog(FrpdDirectoryItem seed, bool isNew, IReadOnlyList existingItems, FrpdDirectoryService service) + { + var viewModel = new FrpdEditWindowViewModel(seed, isNew, existingItems, service); + var window = new FrpdEditWindow(viewModel); + window.Owner = _owner; + + var result = window.ShowDialog(); + return result.HasValue && result.Value ? viewModel.ToResult() : null; + } + + public FrpdvdDirectoryItem ShowFrpdvdEditDialog(FrpdvdDirectoryItem seed, bool isNew, IReadOnlyList existingItems, FrpdDirectoryService service) + { + var viewModel = new FrpdvdEditWindowViewModel(seed, isNew, existingItems, service); + var window = new FrpdvdEditWindow(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 void ShowWarning(string message) + { + MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.OK, MessageBoxImage.Warning); + } + } +} diff --git a/XLAB/FrpdDirectoryModels.cs b/XLAB/FrpdDirectoryModels.cs new file mode 100644 index 0000000..b9654b8 --- /dev/null +++ b/XLAB/FrpdDirectoryModels.cs @@ -0,0 +1,45 @@ +using System; + +namespace XLAB +{ + public sealed class FrpdDirectoryItem + { + public string ActivityNames { get; set; } + + public DateTime? CreatedOn { get; set; } + + public string Guid { get; set; } + + public int Id { get; set; } + + public DateTime? LiquidatedOn { get; set; } + + public string LocalCode { get; set; } + + public string Name { get; set; } + + public int? ParentId { get; set; } + + public string ParentName { get; set; } + } + + public sealed class FrpdvdDirectoryItem + { + public string ActivityName { get; set; } + + public int ActivityId { get; set; } + + public int FrpdId { get; set; } + + public int Id { get; set; } + } + + internal static class FrpdDirectoryRules + { + public const int GuidMaxLength = 50; + + public const int LocalCodeMaxLength = 20; + + public const int NameMaxLength = 80; + } +} diff --git a/XLAB/FrpdDirectoryService.cs b/XLAB/FrpdDirectoryService.cs new file mode 100644 index 0000000..7fe3dba --- /dev/null +++ b/XLAB/FrpdDirectoryService.cs @@ -0,0 +1,556 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq; + +namespace XLAB +{ + internal sealed class FrpdDirectoryService + { + public int AddFrpdItem(FrpdDirectoryItem item) + { + var normalizedItem = NormalizeFrpdItem(item); + + const string sql = @" +INSERT INTO dbo.FRPD +( + IDFRPDR, + NMFRPD, + KDFRPDLC, + FRPDGUID, + DTSZFRPD, + DTLKFRPD +) +VALUES +( + @ParentId, + @Name, + @LocalCode, + @Guid, + @CreatedOn, + @LiquidatedOn +); + +SELECT CAST(SCOPE_IDENTITY() AS int);"; + + using (var connection = ReferenceDirectorySqlHelpers.CreateConnection()) + using (var command = new SqlCommand(sql, connection)) + { + connection.Open(); + EnsureFrpdGuidIsUnique(connection, normalizedItem.Guid, null); + + command.CommandTimeout = 60; + ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ParentId", normalizedItem.ParentId); + command.Parameters.Add("@Name", SqlDbType.VarChar, FrpdDirectoryRules.NameMaxLength).Value = normalizedItem.Name; + ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@LocalCode", SqlDbType.VarChar, FrpdDirectoryRules.LocalCodeMaxLength, normalizedItem.LocalCode); + ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@Guid", SqlDbType.VarChar, FrpdDirectoryRules.GuidMaxLength, normalizedItem.Guid); + ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@CreatedOn", normalizedItem.CreatedOn); + ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@LiquidatedOn", normalizedItem.LiquidatedOn); + + try + { + return Convert.ToInt32(command.ExecuteScalar()); + } + catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "IX_FRPD_FRPDGUID")) + { + throw CreateFrpdDuplicateGuidException(normalizedItem.Guid); + } + } + } + + public int AddFrpdvdItem(FrpdvdDirectoryItem item) + { + var normalizedItem = NormalizeFrpdvdItem(item); + + const string sql = @" +INSERT INTO dbo.FRPDVD +( + IDFRPD, + IDSPVDDO +) +VALUES +( + @FrpdId, + @ActivityId +); + +SELECT CAST(SCOPE_IDENTITY() AS int);"; + + using (var connection = ReferenceDirectorySqlHelpers.CreateConnection()) + using (var command = new SqlCommand(sql, connection)) + { + connection.Open(); + EnsureFrpdvdIsUnique(connection, normalizedItem.FrpdId, normalizedItem.ActivityId, null); + + command.CommandTimeout = 60; + command.Parameters.Add("@FrpdId", SqlDbType.Int).Value = normalizedItem.FrpdId; + command.Parameters.Add("@ActivityId", SqlDbType.Int).Value = normalizedItem.ActivityId; + + try + { + return Convert.ToInt32(command.ExecuteScalar()); + } + catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "XAK1FRPDVD")) + { + throw CreateFrpdvdDuplicateException(); + } + } + } + + public DirectoryDeleteResult DeleteFrpdItem(int id) + { + if (id <= 0) + { + throw new InvalidOperationException("Не выбрана запись FRPD для удаления."); + } + + const string sql = @" +DELETE FROM dbo.FRPD +WHERE IDFRPD = @Id; + +SELECT @@ROWCOUNT;"; + + try + { + using (var connection = ReferenceDirectorySqlHelpers.CreateConnection()) + { + connection.Open(); + var blockers = ReferenceDirectorySqlHelpers.LoadDeleteBlockersFromForeignKeys(connection, "FRPD", id); + if (blockers.Count > 0) + { + return new DirectoryDeleteResult + { + IsDeleted = false, + WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPD", 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("Запись FRPD для удаления не найдена."); + } + } + + return new DirectoryDeleteResult + { + IsDeleted = true + }; + } + } + catch (SqlException ex) when (ex.Number == 547) + { + return new DirectoryDeleteResult + { + IsDeleted = false, + WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPD", ex) + }; + } + } + + public DirectoryDeleteResult DeleteFrpdvdItem(int id) + { + if (id <= 0) + { + throw new InvalidOperationException("Не выбрана запись FRPDVD для удаления."); + } + + const string sql = @" +DELETE FROM dbo.FRPDVD +WHERE IDFRPDVD = @Id; + +SELECT @@ROWCOUNT;"; + + try + { + using (var connection = ReferenceDirectorySqlHelpers.CreateConnection()) + { + connection.Open(); + var blockers = ReferenceDirectorySqlHelpers.LoadDeleteBlockersFromForeignKeys(connection, "FRPDVD", id); + if (blockers.Count > 0) + { + return new DirectoryDeleteResult + { + IsDeleted = false, + WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPDVD", 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("Запись FRPDVD для удаления не найдена."); + } + } + + return new DirectoryDeleteResult + { + IsDeleted = true + }; + } + } + catch (SqlException ex) when (ex.Number == 547) + { + return new DirectoryDeleteResult + { + IsDeleted = false, + WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPDVD", ex) + }; + } + } + + public IReadOnlyList LoadFrpdItems() + { + const string sql = @" +SELECT + fr.IDFRPD AS Id, + fr.IDFRPDR AS ParentId, + parent.NMFRPD AS ParentName, + fr.NMFRPD AS Name, + fr.KDFRPDLC AS LocalCode, + fr.FRPDGUID AS Guid, + fr.DTSZFRPD AS CreatedOn, + fr.DTLKFRPD AS LiquidatedOn, + ISNULL( + STUFF(( + SELECT ', ' + activity.ActivityName + FROM + ( + SELECT DISTINCT sp.NMVDDO AS ActivityName + FROM dbo.FRPDVD link + JOIN dbo.SPVDDO sp ON sp.IDSPVDDO = link.IDSPVDDO + WHERE link.IDFRPD = fr.IDFRPD + ) activity + ORDER BY activity.ActivityName + FOR XML PATH(''), TYPE + ).value('.', 'nvarchar(max)'), 1, 2, ''), + '' + ) AS ActivityNames +FROM dbo.FRPD fr +LEFT JOIN dbo.FRPD parent ON parent.IDFRPD = fr.IDFRPDR +ORDER BY fr.NMFRPD, fr.IDFRPD;"; + + var items = new List(); + + using (var connection = ReferenceDirectorySqlHelpers.CreateConnection()) + using (var command = new SqlCommand(sql, connection)) + { + connection.Open(); + command.CommandTimeout = 60; + + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + items.Add(new FrpdDirectoryItem + { + ActivityNames = ReferenceDirectorySqlHelpers.GetString(reader, "ActivityNames"), + CreatedOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "CreatedOn"), + Guid = ReferenceDirectorySqlHelpers.GetString(reader, "Guid"), + Id = ReferenceDirectorySqlHelpers.GetInt32(reader, "Id"), + LiquidatedOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "LiquidatedOn"), + LocalCode = ReferenceDirectorySqlHelpers.GetString(reader, "LocalCode"), + Name = ReferenceDirectorySqlHelpers.GetString(reader, "Name"), + ParentId = ReferenceDirectorySqlHelpers.GetNullableInt32(reader, "ParentId"), + ParentName = ReferenceDirectorySqlHelpers.GetString(reader, "ParentName") + }); + } + } + } + + return items; + } + + public IReadOnlyList LoadFrpdReferences() + { + return ReferenceDirectorySqlHelpers.LoadLookupItems(@" +SELECT + fr.IDFRPD AS Id, + fr.NMFRPD AS Name +FROM dbo.FRPD fr +ORDER BY fr.NMFRPD, fr.IDFRPD;"); + } + + public IReadOnlyList LoadFrpdvdItems(int frpdId) + { + const string sql = @" +SELECT + link.IDFRPDVD AS Id, + link.IDFRPD AS FrpdId, + link.IDSPVDDO AS ActivityId, + sp.NMVDDO AS ActivityName +FROM dbo.FRPDVD link +JOIN dbo.SPVDDO sp ON sp.IDSPVDDO = link.IDSPVDDO +WHERE link.IDFRPD = @FrpdId +ORDER BY sp.NMVDDO, link.IDFRPDVD;"; + + var items = new List(); + + using (var connection = ReferenceDirectorySqlHelpers.CreateConnection()) + using (var command = new SqlCommand(sql, connection)) + { + connection.Open(); + command.CommandTimeout = 60; + command.Parameters.Add("@FrpdId", SqlDbType.Int).Value = frpdId; + + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + items.Add(new FrpdvdDirectoryItem + { + ActivityId = ReferenceDirectorySqlHelpers.GetInt32(reader, "ActivityId"), + ActivityName = ReferenceDirectorySqlHelpers.GetString(reader, "ActivityName"), + FrpdId = ReferenceDirectorySqlHelpers.GetInt32(reader, "FrpdId"), + Id = ReferenceDirectorySqlHelpers.GetInt32(reader, "Id") + }); + } + } + } + + return items; + } + + public IReadOnlyList LoadSpvddoReferences() + { + return ReferenceDirectorySqlHelpers.LoadLookupItems(@" +SELECT + sp.IDSPVDDO AS Id, + sp.NMVDDO AS Name +FROM dbo.SPVDDO sp +ORDER BY sp.NMVDDO, sp.IDSPVDDO;"); + } + + public void UpdateFrpdItem(FrpdDirectoryItem item) + { + var normalizedItem = NormalizeFrpdItem(item); + if (normalizedItem.Id <= 0) + { + throw new InvalidOperationException("Не выбрана запись FRPD для изменения."); + } + + const string sql = @" +UPDATE dbo.FRPD +SET IDFRPDR = @ParentId, + NMFRPD = @Name, + KDFRPDLC = @LocalCode, + FRPDGUID = @Guid, + DTSZFRPD = @CreatedOn, + DTLKFRPD = @LiquidatedOn +WHERE IDFRPD = @Id; + +SELECT @@ROWCOUNT;"; + + using (var connection = ReferenceDirectorySqlHelpers.CreateConnection()) + using (var command = new SqlCommand(sql, connection)) + { + connection.Open(); + EnsureFrpdGuidIsUnique(connection, normalizedItem.Guid, normalizedItem.Id); + + command.CommandTimeout = 60; + command.Parameters.Add("@Id", SqlDbType.Int).Value = normalizedItem.Id; + ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ParentId", normalizedItem.ParentId); + command.Parameters.Add("@Name", SqlDbType.VarChar, FrpdDirectoryRules.NameMaxLength).Value = normalizedItem.Name; + ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@LocalCode", SqlDbType.VarChar, FrpdDirectoryRules.LocalCodeMaxLength, normalizedItem.LocalCode); + ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@Guid", SqlDbType.VarChar, FrpdDirectoryRules.GuidMaxLength, normalizedItem.Guid); + ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@CreatedOn", normalizedItem.CreatedOn); + ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@LiquidatedOn", normalizedItem.LiquidatedOn); + + try + { + if (Convert.ToInt32(command.ExecuteScalar()) == 0) + { + throw new InvalidOperationException("Запись FRPD для изменения не найдена."); + } + } + catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "IX_FRPD_FRPDGUID")) + { + throw CreateFrpdDuplicateGuidException(normalizedItem.Guid); + } + } + } + + public void UpdateFrpdvdItem(FrpdvdDirectoryItem item) + { + var normalizedItem = NormalizeFrpdvdItem(item); + if (normalizedItem.Id <= 0) + { + throw new InvalidOperationException("Не выбрана запись FRPDVD для изменения."); + } + + const string sql = @" +UPDATE dbo.FRPDVD +SET IDSPVDDO = @ActivityId +WHERE IDFRPDVD = @Id; + +SELECT @@ROWCOUNT;"; + + using (var connection = ReferenceDirectorySqlHelpers.CreateConnection()) + using (var command = new SqlCommand(sql, connection)) + { + connection.Open(); + EnsureFrpdvdIsUnique(connection, normalizedItem.FrpdId, normalizedItem.ActivityId, normalizedItem.Id); + + command.CommandTimeout = 60; + command.Parameters.Add("@Id", SqlDbType.Int).Value = normalizedItem.Id; + command.Parameters.Add("@ActivityId", SqlDbType.Int).Value = normalizedItem.ActivityId; + + try + { + if (Convert.ToInt32(command.ExecuteScalar()) == 0) + { + throw new InvalidOperationException("Запись FRPDVD для изменения не найдена."); + } + } + catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "XAK1FRPDVD")) + { + throw CreateFrpdvdDuplicateException(); + } + } + } + + private static InvalidOperationException CreateFrpdDuplicateGuidException(string guid) + { + return new InvalidOperationException(string.Format("GUID подразделения \"{0}\" уже существует в справочнике.", guid)); + } + + private static InvalidOperationException CreateFrpdvdDuplicateException() + { + return new InvalidOperationException("Выбранный вид деятельности уже существует у этой организации/подразделения."); + } + + private static void EnsureFrpdGuidIsUnique(SqlConnection connection, string guid, int? excludeId) + { + if (string.IsNullOrWhiteSpace(guid)) + { + return; + } + + const string sql = @" +SELECT COUNT(1) +FROM dbo.FRPD +WHERE FRPDGUID = @Guid + AND (@ExcludeId IS NULL OR IDFRPD <> @ExcludeId);"; + + using (var command = new SqlCommand(sql, connection)) + { + command.CommandTimeout = 60; + command.Parameters.Add("@Guid", SqlDbType.VarChar, FrpdDirectoryRules.GuidMaxLength).Value = guid; + ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ExcludeId", excludeId); + if (Convert.ToInt32(command.ExecuteScalar()) > 0) + { + throw CreateFrpdDuplicateGuidException(guid); + } + } + } + + private static void EnsureFrpdvdIsUnique(SqlConnection connection, int frpdId, int activityId, int? excludeId) + { + const string sql = @" +SELECT COUNT(1) +FROM dbo.FRPDVD +WHERE IDFRPD = @FrpdId + AND IDSPVDDO = @ActivityId + AND (@ExcludeId IS NULL OR IDFRPDVD <> @ExcludeId);"; + + using (var command = new SqlCommand(sql, connection)) + { + command.CommandTimeout = 60; + command.Parameters.Add("@FrpdId", SqlDbType.Int).Value = frpdId; + command.Parameters.Add("@ActivityId", SqlDbType.Int).Value = activityId; + ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ExcludeId", excludeId); + if (Convert.ToInt32(command.ExecuteScalar()) > 0) + { + throw CreateFrpdvdDuplicateException(); + } + } + } + + private static string NormalizeRequired(string value, int maxLength, string fieldDisplayName) + { + var normalizedValue = string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); + if (normalizedValue.Length == 0) + { + throw new InvalidOperationException(string.Format("Укажите {0}.", fieldDisplayName)); + } + + if (normalizedValue.Length > maxLength) + { + throw new InvalidOperationException(string.Format("{0} не должно превышать {1} символов.", fieldDisplayName, maxLength)); + } + + return normalizedValue; + } + + private static string NormalizeNullable(string value, int maxLength, string fieldDisplayName) + { + var normalizedValue = string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + if (normalizedValue != null && normalizedValue.Length > maxLength) + { + throw new InvalidOperationException(string.Format("{0} не должно превышать {1} символов.", fieldDisplayName, maxLength)); + } + + return normalizedValue; + } + + private static FrpdDirectoryItem NormalizeFrpdItem(FrpdDirectoryItem item) + { + if (item == null) + { + throw new InvalidOperationException("Не передана запись FRPD."); + } + + var normalizedItem = new FrpdDirectoryItem + { + CreatedOn = item.CreatedOn, + Guid = NormalizeNullable(item.Guid, FrpdDirectoryRules.GuidMaxLength, "GUID подразделения"), + Id = item.Id, + LiquidatedOn = item.LiquidatedOn, + LocalCode = NormalizeNullable(item.LocalCode, FrpdDirectoryRules.LocalCodeMaxLength, "Локальный код организации/подразделения"), + Name = NormalizeRequired(item.Name, FrpdDirectoryRules.NameMaxLength, "организацию/подразделение"), + ParentId = item.ParentId.HasValue && item.ParentId.Value > 0 ? item.ParentId : null + }; + + if (normalizedItem.Id > 0 + && normalizedItem.ParentId.HasValue + && normalizedItem.ParentId.Value == normalizedItem.Id) + { + throw new InvalidOperationException("Организация/подразделение не может ссылаться на себя как на родительскую запись."); + } + + return normalizedItem; + } + + private static FrpdvdDirectoryItem NormalizeFrpdvdItem(FrpdvdDirectoryItem item) + { + if (item == null) + { + throw new InvalidOperationException("Не передана запись FRPDVD."); + } + + if (item.FrpdId <= 0) + { + throw new InvalidOperationException("Не выбрана организация/подразделение для вида деятельности."); + } + + if (item.ActivityId <= 0) + { + throw new InvalidOperationException("Укажите вид деятельности организации."); + } + + return new FrpdvdDirectoryItem + { + ActivityId = item.ActivityId, + FrpdId = item.FrpdId, + Id = item.Id + }; + } + } +} diff --git a/XLAB/FrpdDirectoryWindow.xaml b/XLAB/FrpdDirectoryWindow.xaml new file mode 100644 index 0000000..4d5c806 --- /dev/null +++ b/XLAB/FrpdDirectoryWindow.xaml @@ -0,0 +1,119 @@ + + + + + + + + + + + +