diff --git a/SQLVision/Models/ITreeNode.cs b/SQLVision/Models/ITreeNode.cs new file mode 100644 index 0000000..2826982 --- /dev/null +++ b/SQLVision/Models/ITreeNode.cs @@ -0,0 +1,8 @@ +namespace SQLVision.Models +{ + public interface ITreeNode + { + bool IsFolder { get; } + string Name { get; set; } + } +} \ No newline at end of file diff --git a/SQLVision/Models/ScriptTreeNode.cs b/SQLVision/Models/ScriptTreeNode.cs index 0d7af09..cd3a8b0 100644 --- a/SQLVision/Models/ScriptTreeNode.cs +++ b/SQLVision/Models/ScriptTreeNode.cs @@ -2,7 +2,7 @@ namespace SQLVision.Models; -public sealed class ScriptTreeNode +public sealed class ScriptTreeNode : ITreeNode { public string Name { get; set; } = ""; public string? FilePath { get; set; } diff --git a/SQLVision/SQLVision.csproj b/SQLVision/SQLVision.csproj index ec026ea..a669874 100644 --- a/SQLVision/SQLVision.csproj +++ b/SQLVision/SQLVision.csproj @@ -17,6 +17,7 @@ + @@ -48,6 +49,9 @@ Always + + MSBuild:Compile + MSBuild:Compile diff --git a/SQLVision/Selectors/TreeItemTemplateSelector.cs b/SQLVision/Selectors/TreeItemTemplateSelector.cs new file mode 100644 index 0000000..382ef25 --- /dev/null +++ b/SQLVision/Selectors/TreeItemTemplateSelector.cs @@ -0,0 +1,18 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using SQLVision.Models; + +namespace SQLVision.Selectors; + +class TreeItemTemplateSelector : DataTemplateSelector +{ + public DataTemplate FolderTemplate { get; set; } + + public DataTemplate FileTemplate { get; set; } + + protected override DataTemplate SelectTemplateCore(object item) + { + var explorerItem = (ITreeNode)item; + return explorerItem.IsFolder ? FolderTemplate : FileTemplate; + } +} \ No newline at end of file diff --git a/SQLVision/Services/SqlExecutor.cs b/SQLVision/Services/SqlExecutor.cs index 8f5d9c7..6ed7b22 100644 --- a/SQLVision/Services/SqlExecutor.cs +++ b/SQLVision/Services/SqlExecutor.cs @@ -10,12 +10,7 @@ namespace SQLVision.Services; public sealed class SqlExecutor { - private string _connectionString; - - public SqlExecutor(string connectionString) - { - _connectionString = connectionString; - } + private string _connectionString = ""; public void SetConnect(string connectionString) { diff --git a/SQLVision/Services/TableDataProvider.cs b/SQLVision/Services/TableDataProvider.cs index 58b5a7b..e76e83b 100644 --- a/SQLVision/Services/TableDataProvider.cs +++ b/SQLVision/Services/TableDataProvider.cs @@ -7,9 +7,9 @@ namespace SQLVision.Services; public sealed class TableDataProvider { - private readonly string _connectionString; + private string _connectionString = ""; - public TableDataProvider(string connectionString) + public void SetConnect(string connectionString) { _connectionString = connectionString; } diff --git a/SQLVision/ViewModels/ParameterViewModel.cs b/SQLVision/ViewModels/ParameterViewModel.cs index ae6a11b..cfba19d 100644 --- a/SQLVision/ViewModels/ParameterViewModel.cs +++ b/SQLVision/ViewModels/ParameterViewModel.cs @@ -41,6 +41,16 @@ public sealed class ParameterViewModel : BaseViewModel public ParameterViewModel(ParameterDefinition definition) { Definition = definition; - Value = definition.DefaultValue; + + if (definition.DefaultValue is not null) + { + Value = definition.Type switch + { + ParameterType.Bool => definition.DefaultValue.Equals("true", System.StringComparison.OrdinalIgnoreCase) || definition.DefaultValue.Equals("1", System.StringComparison.OrdinalIgnoreCase), + ParameterType.Int => int.TryParse(definition.DefaultValue, out var i) ? i : null, + ParameterType.String => definition.DefaultValue, + _ => definition.DefaultValue, + }; + } } } diff --git a/SQLVision/ViewModels/ScriptsViewModel.cs b/SQLVision/ViewModels/ScriptsViewModel.cs new file mode 100644 index 0000000..ae859d5 --- /dev/null +++ b/SQLVision/ViewModels/ScriptsViewModel.cs @@ -0,0 +1,328 @@ +using Microsoft.UI.Xaml; +using SQLVision.Models; +using SQLVision.Services; +using SQLVision.Views; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace SQLVision.ViewModels; + +public sealed partial class ScriptsViewModel : BaseViewModel +{ + private readonly SqlCommentParser _parser; + private readonly SqlExecutor _executor; + private readonly ParameterResolver _parameterResolver; + private readonly TableDataProvider _tableDataProvider; + private readonly ExcelExportService _excelExportService; + + // ----------------------------- + // Подключения + // ----------------------------- + private readonly ConnectionStorageService _storage; + + public ObservableCollection SavedConnections { get; } = new(); + + private ConnectionProfile? _selectedConnection; + public ConnectionProfile? SelectedConnection + { + get => _selectedConnection; + set + { + if (SetProperty(ref _selectedConnection, value)) + { + if (value != null) + ConnectionString = value.ConnectionString; + } + } + } + + private string _connectionString = ""; + public string ConnectionString + { + get => _connectionString; + set => SetProperty(ref _connectionString, value); + } + + // ----------------------------- + // Скрипты, дерево и параметры + // ----------------------------- + public ObservableCollection Scripts { get; } = new(); + + // Дерево скриптов для TreeView + public ObservableCollection ScriptTree { get; } = new(); + + private ScriptTreeNode? _selectedScriptNode; + public ScriptTreeNode? SelectedScriptNode + { + get => _selectedScriptNode; + set + { + if (SetProperty(ref _selectedScriptNode, value)) + { + if (value?.FilePath is not null) + { + // Находим ScriptMetadata по пути файла + var meta = Scripts.FirstOrDefault(s => + string.Equals(s.FilePath, value.FilePath, StringComparison.OrdinalIgnoreCase)); + + if (meta is not null) + SelectedScript = meta; + } + } + } + } + + public ObservableCollection Parameters { get; } = new(); + public ObservableCollection Outputs { get; } = new(); + + private ScriptMetadata? _selectedScript; + public ScriptMetadata? SelectedScript + { + get => _selectedScript; + set + { + if (SetProperty(ref _selectedScript, value)) + { + (ExecuteCommand as RelayCommand)?.RaiseCanExecuteChanged(); + _ = LoadParametersForSelectedScriptAsync(); + } + } + } + + private ExecutionResult? _lastResult; + public ExecutionResult? LastResult + { + get => _lastResult; + set + { + if (SetProperty(ref _lastResult, value)) + { + (ExportToExcelCommand as RelayCommand)?.RaiseCanExecuteChanged(); + _ = LoadParametersForSelectedScriptAsync(); + } + } + } + + // ----------------------------- + // Команды + // ----------------------------- + public ICommand ExecuteCommand { get; } + public ICommand ExportToExcelCommand { get; } + public ICommand OpenConnectionCommand { get; } + + // ----------------------------- + // Конструктор + // ----------------------------- + public ScriptsViewModel( + SqlCommentParser parser, + SqlExecutor executor, + ParameterResolver parameterResolver, + TableDataProvider tableDataProvider, + ExcelExportService excelExportService) + { + _parser = parser; + _executor = executor; + _parameterResolver = parameterResolver; + _tableDataProvider = tableDataProvider; + _excelExportService = excelExportService; + + ExecuteCommand = new RelayCommand(async _ => await ExecuteAsync(), _ => SelectedScript is not null); + ExportToExcelCommand = new RelayCommand(_ => ExportToExcel(), _ => LastResult is not null); + OpenConnectionCommand = new RelayCommand(_ => OpenConnection()); + + _storage = new ConnectionStorageService(); + + foreach (var p in _storage.Load()) + SavedConnections.Add(p); + + SelectedConnection = SavedConnections.FirstOrDefault(); + } + + // ----------------------------- + // Окно подключения + // ----------------------------- + private void OpenConnection() + { + var vm = new ConnectionViewModel(); + var page = new ConnectionPage { DataContext = vm }; + + vm.ConnectionSaved += profile => + { + var existing = SavedConnections.FirstOrDefault(p => p.Name == profile.Name); + if (existing is not null) + SavedConnections.Remove(existing); + + SavedConnections.Add(profile); + _storage.Save(SavedConnections.ToList()); + SelectedConnection = profile; + }; + + var window = new Window + { + Content = page + }; + window.Activate(); + } + + // ----------------------------- + // Загрузка скриптов и построение дерева + // ----------------------------- + public void LoadScripts(string scriptsFolder) + { + Scripts.Clear(); + ScriptTree.Clear(); + + if (!Directory.Exists(scriptsFolder)) + return; + + // Строим дерево по корневой папке + var rootNode = BuildScriptTree(scriptsFolder); + ScriptTree.Add(rootNode); + } + + private ScriptTreeNode BuildScriptTree(string folderPath) + { + var folderNode = new ScriptTreeNode + { + Name = Path.GetFileName(folderPath), + FilePath = null + }; + + // Подпапки + foreach (var dir in Directory.GetDirectories(folderPath)) + { + var subNode = BuildScriptTree(dir); + folderNode.Children.Add(subNode); + } + + // Файлы .sql + foreach (var file in Directory.GetFiles(folderPath, "*.sql")) + { + var meta = _parser.Parse(file); + Scripts.Add(meta); + + var fileNode = new ScriptTreeNode + { + Name = Path.GetFileName(file), + FilePath = file + }; + + folderNode.Children.Add(fileNode); + } + + return folderNode; + } + + // ----------------------------- + // Параметры + // ----------------------------- + private async Task LoadParametersForSelectedScriptAsync() + { + Parameters.Clear(); + Outputs.Clear(); + + if (SelectedScript is null) + return; + + foreach (var p in SelectedScript.Parameters) + { + var vm = new ParameterViewModel(p); + Parameters.Add(vm); + + if (p.TableQuery is not null) + { + var table = await _tableDataProvider.LoadTableAsync(p.TableQuery); + vm.LookupTable = table; + } + } + + foreach (var o in SelectedScript.Outputs) + Outputs.Add(new OutputViewModel(o)); + } + + // ----------------------------- + // Выполнение SQL + // ----------------------------- + private async Task ExecuteAsync() + { + if (SelectedScript is null) + return; + + if (string.IsNullOrWhiteSpace(ConnectionString)) + { + // TODO: вывести ContentDialog "Подключение не выбрано" + return; + } + + var sqlText = await File.ReadAllTextAsync(SelectedScript.FilePath); + + var paramValues = Parameters.ToDictionary( + p => p.Definition, + p => p.Value); + + // Если SqlExecutor умеет менять строку подключения динамически — здесь можно прокинуть ConnectionString + _executor.SetConnect(ConnectionString); + var result = await _executor.ExecuteAsync(SelectedScript, sqlText, paramValues); + + for (int i = 0; i < Outputs.Count; i++) + { + result.Tables[i].TableName = Outputs[i].Definition.Title; + Outputs[i].Table = result.Tables[i]; + } + + LastResult = result; + + + RaisePropertyChanged(nameof(Outputs)); + + SaveParameterValues(SelectedScript.FilePath, paramValues); + } + + // ----------------------------- + // Экспорт + // ----------------------------- + private void ExportToExcel() + { + if (LastResult is null || LastResult.Tables.Count == 0) + return; + + var directory = Path.Combine(AppContext.BaseDirectory, "Export"); + Directory.CreateDirectory(directory); + + var fileName = $"{LastResult.ScriptName}_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx"; + var fullPath = Path.Combine(directory, fileName); + + _excelExportService.ExportTablesToExcel(LastResult.Tables, fullPath); + } + + // ----------------------------- + // Сохранение параметров + // ----------------------------- + private void SaveParameterValues(string scriptPath, Dictionary values) + { + var directory = Path.Combine(AppContext.BaseDirectory, "Config", "Parameters"); + Directory.CreateDirectory(directory); + + var fileName = Path.GetFileNameWithoutExtension(scriptPath) + ".json"; + var fullPath = Path.Combine(directory, fileName); + + var payload = new ParameterValueSet + { + ScriptFilePath = scriptPath, + Values = values.ToDictionary(t => t.Key.Name, t => t.Value), + }; + + var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions + { + WriteIndented = true + }); + + File.WriteAllText(fullPath, json); + } +} diff --git a/SQLVision/Views/MainWindow.xaml b/SQLVision/Views/MainWindow.xaml index 8ce0336..3512e5d 100644 --- a/SQLVision/Views/MainWindow.xaml +++ b/SQLVision/Views/MainWindow.xaml @@ -9,223 +9,46 @@ - + - - + - - + + + - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -