diff --git a/src/ElectronicPharmacy/Classes/ConsigneeHelper.cs b/src/ElectronicPharmacy/Classes/ConsigneeHelper.cs new file mode 100644 index 0000000..4c91df2 --- /dev/null +++ b/src/ElectronicPharmacy/Classes/ConsigneeHelper.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Data.SQLite; + +namespace Электронная_Фармация.Classes +{ + /// + /// Аптека = грузополучатель + LocationId. Прайс общий, заказы уходят с location_id выбранной аптеки. + /// + public static class ConsigneeHelper + { + public static string GetDefaultConsigneeName() + { + try + { + using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + connection.Open(); + using (var command = new SQLiteCommand( + @"SELECT ConsigneesName + FROM Consignees + ORDER BY ConsigneesUseAsDefault DESC, ConsigneesName + LIMIT 1", + connection)) + { + var result = command.ExecuteScalar(); + return result == null || result == DBNull.Value + ? string.Empty + : result.ToString().Trim(); + } + } + } + catch + { + return string.Empty; + } + } + + public static string GetLocationIdByName(string consigneeName) + { + if (string.IsNullOrWhiteSpace(consigneeName)) + { + return string.Empty; + } + + using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + connection.Open(); + using (var command = new SQLiteCommand( + @"SELECT ifnull(LocationId, '') + FROM Consignees + WHERE ConsigneesName = @name + LIMIT 1", + connection)) + { + command.Parameters.AddWithValue("@name", consigneeName.Trim()); + var result = command.ExecuteScalar(); + return result == null || result == DBNull.Value + ? string.Empty + : result.ToString().Trim(); + } + } + } + + public static string ResolveLocationId(string consigneeName) + { + var fromConsignee = GetLocationIdByName(consigneeName); + if (!string.IsNullOrWhiteSpace(fromConsignee)) + { + return fromConsignee; + } + + return AppConfig.LocationId ?? string.Empty; + } + + /// + /// Помечает аптеку активной и синхронизирует AppConfig.LocationId. + /// + public static void SetActiveConsignee(string consigneeName) + { + if (string.IsNullOrWhiteSpace(consigneeName)) + { + return; + } + + var name = consigneeName.Trim(); + using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + connection.Open(); + using (var tx = connection.BeginTransaction()) + { + using (var clear = new SQLiteCommand( + "UPDATE Consignees SET ConsigneesUseAsDefault = 0", + connection, + tx)) + { + clear.ExecuteNonQuery(); + } + + using (var setDefault = new SQLiteCommand( + @"UPDATE Consignees + SET ConsigneesUseAsDefault = 1 + WHERE ConsigneesName = @name", + connection, + tx)) + { + setDefault.Parameters.AddWithValue("@name", name); + setDefault.ExecuteNonQuery(); + } + + tx.Commit(); + } + + var locationId = GetLocationIdByName(name); + if (!string.IsNullOrWhiteSpace(locationId)) + { + AppConfig.SetLocationId(locationId); + } + } + } + + public static void SetLocationId(string consigneeName, string locationId) + { + if (string.IsNullOrWhiteSpace(consigneeName)) + { + return; + } + + using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + connection.Open(); + using (var command = new SQLiteCommand( + @"UPDATE Consignees + SET LocationId = @loc + WHERE ConsigneesName = @name", + connection)) + { + command.Parameters.AddWithValue("@loc", (locationId ?? string.Empty).Trim()); + command.Parameters.AddWithValue("@name", consigneeName.Trim()); + command.ExecuteNonQuery(); + } + } + } + + public static void MigrateGlobalLocationIdIfNeeded(SQLiteConnection connection) + { + var global = AppConfig.LocationId; + if (string.IsNullOrWhiteSpace(global)) + { + return; + } + + using (var command = new SQLiteCommand( + @"UPDATE Consignees + SET LocationId = @loc + WHERE (LocationId IS NULL OR trim(LocationId) = '') + AND ( + ConsigneesUseAsDefault = 1 + OR (SELECT COUNT(*) FROM Consignees) = 1 + )", + connection)) + { + command.Parameters.AddWithValue("@loc", global.Trim()); + command.ExecuteNonQuery(); + } + } + + public static void ApplyLocationIdToEmptyConsignees(string locationId) + { + if (string.IsNullOrWhiteSpace(locationId)) + { + return; + } + + using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + connection.Open(); + using (var command = new SQLiteCommand( + @"UPDATE Consignees + SET LocationId = @loc + WHERE LocationId IS NULL OR trim(LocationId) = ''", + connection)) + { + command.Parameters.AddWithValue("@loc", locationId.Trim()); + command.ExecuteNonQuery(); + } + } + } + + public static IList GetKnownSupplierIds() + { + var result = new List(); + using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + connection.Open(); + using (var command = new SQLiteCommand( + @"SELECT DISTINCT trim(supplier_id) + FROM PriceList + WHERE supplier_id IS NOT NULL AND trim(supplier_id) <> '' + ORDER BY 1", + connection)) + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetString(0); + if (!string.IsNullOrWhiteSpace(id)) + { + result.Add(id); + } + } + } + } + + return result; + } + } +} diff --git a/src/ElectronicPharmacy/Classes/DataSender.cs b/src/ElectronicPharmacy/Classes/DataSender.cs index 941e0f3..0d16877 100644 --- a/src/ElectronicPharmacy/Classes/DataSender.cs +++ b/src/ElectronicPharmacy/Classes/DataSender.cs @@ -35,20 +35,6 @@ namespace Электронная_Фармация.Classes return; } - if (string.IsNullOrWhiteSpace(AppConfig.LocationId)) - { - if (!HF_LocationId.PromptAndSave(Form.ActiveForm) - || string.IsNullOrWhiteSpace(AppConfig.LocationId)) - { - AppDebugLog.Error("DataSender", "Отправка заказов: location_id не указан"); - ToastNotification.ShowCustom( - "Не указан location_id. Заполните его после авторизации.", - Color.DarkOrange, - Color.White); - return; - } - } - List orders; try { @@ -66,6 +52,11 @@ namespace Электронная_Фармация.Classes return; } + if (!EnsureOrdersHaveLocationId(orders)) + { + return; + } + int successCount = 0; int failCount = 0; @@ -110,20 +101,6 @@ namespace Электронная_Фармация.Classes return false; } - if (string.IsNullOrWhiteSpace(AppConfig.LocationId)) - { - if (!HF_LocationId.PromptAndSave(Form.ActiveForm) - || string.IsNullOrWhiteSpace(AppConfig.LocationId)) - { - AppDebugLog.Error("DataSender", $"Отправка заказа {localOrderId}: location_id не указан"); - ToastNotification.ShowCustom( - "Не указан location_id. Заполните его после авторизации.", - Color.DarkOrange, - Color.White); - return false; - } - } - List orders; try { @@ -141,6 +118,11 @@ namespace Электронная_Фармация.Classes return false; } + if (!EnsureOrdersHaveLocationId(orders)) + { + return false; + } + bool ok = await SendOrderAsync(orders[0]); if (ok) { @@ -153,7 +135,7 @@ namespace Электронная_Фармация.Classes public List FetchDataFromSql(string onlyOrderId) { var orders = new Dictionary(); - string locationId = AppConfig.LocationId; + var locationCache = new Dictionary(StringComparer.OrdinalIgnoreCase); using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) { @@ -161,6 +143,7 @@ namespace Электронная_Фармация.Classes string query = @" SELECT Orders.Id_Order, + Orders.ConsigneeName, OrderItems.supplier_price_id, OrderItems.DrugName, OrderItems.es_code, @@ -189,6 +172,9 @@ WHERE Orders.OrderState = 'НОВЫЙ' while (reader.Read()) { string orderId = reader["Id_Order"].ToString().Trim(); + string consigneeName = reader["ConsigneeName"] == DBNull.Value + ? string.Empty + : reader["ConsigneeName"].ToString().Trim(); string supplierPriceId = reader["supplier_price_id"].ToString().Trim(); string drugName = reader["DrugName"] == DBNull.Value ? null : reader["DrugName"].ToString().Trim(); string esCode = reader["es_code"] == DBNull.Value ? null : reader["es_code"].ToString().Trim(); @@ -206,6 +192,7 @@ WHERE Orders.OrderState = 'НОВЫЙ' if (!orders.TryGetValue(orderId, out BuyerOrderRequest order)) { + string locationId = ResolveLocationIdCached(consigneeName, locationCache); order = new BuyerOrderRequest { LocalOrderId = orderId, @@ -230,6 +217,65 @@ WHERE Orders.OrderState = 'НОВЫЙ' return orders.Values.ToList(); } + private static string ResolveLocationIdCached(string consigneeName, Dictionary cache) + { + var key = consigneeName ?? string.Empty; + if (cache.TryGetValue(key, out var cached)) + { + return cached; + } + + var locationId = ConsigneeHelper.ResolveLocationId(consigneeName); + cache[key] = locationId ?? string.Empty; + return cache[key]; + } + + private bool EnsureOrdersHaveLocationId(List orders) + { + var missing = orders + .Where(o => string.IsNullOrWhiteSpace(o.LocationId)) + .Select(o => o.LocalOrderId) + .Distinct() + .ToList(); + + if (missing.Count == 0) + { + return true; + } + + if (string.IsNullOrWhiteSpace(AppConfig.LocationId)) + { + if (!HF_LocationId.PromptAndSave(Form.ActiveForm) + || string.IsNullOrWhiteSpace(AppConfig.LocationId)) + { + AppDebugLog.Error("DataSender", "Отправка заказов: location_id не указан"); + ToastNotification.ShowCustom( + "Не указан Location ID у аптеки заказа. Задайте его в «Грузополучатели» или в настройках.", + Color.DarkOrange, + Color.White); + return false; + } + + ConsigneeHelper.ApplyLocationIdToEmptyConsignees(AppConfig.LocationId); + } + + foreach (var order in orders.Where(o => string.IsNullOrWhiteSpace(o.LocationId))) + { + order.LocationId = AppConfig.LocationId; + } + + if (orders.Any(o => string.IsNullOrWhiteSpace(o.LocationId))) + { + ToastNotification.ShowCustom( + "У части заказов нет Location ID. Укажите ID у грузополучателя.", + Color.DarkOrange, + Color.White); + return false; + } + + return true; + } + private async Task SendOrderAsync(BuyerOrderRequest order) { var options = new JsonSerializerOptions diff --git a/src/ElectronicPharmacy/Classes/SqlSyntaxHighlighter.cs b/src/ElectronicPharmacy/Classes/SqlSyntaxHighlighter.cs new file mode 100644 index 0000000..8c42715 --- /dev/null +++ b/src/ElectronicPharmacy/Classes/SqlSyntaxHighlighter.cs @@ -0,0 +1,153 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Windows.Forms; + +namespace Электронная_Фармация.Classes +{ + /// + /// Лёгкая подсветка SQL в RichTextBox (ключевые слова, строки, комментарии, числа, параметры). + /// + public static class SqlSyntaxHighlighter + { + private static readonly Regex TokenRegex = new Regex( + @"(--[^\r\n]*)|(/\*[\s\S]*?\*/)|('(?:''|[^'])*')|(""(?:""""|[^""])*"")|(\[[^\]]*\])|(@[A-Za-z_][A-Za-z0-9_]*)|(\{\{[A-Za-z_][A-Za-z0-9_]*\}\})|(\b\d+(?:\.\d+)?\b)|(\b[A-Za-z_][A-Za-z0-9_]*\b)", + RegexOptions.Compiled); + + private static readonly System.Collections.Generic.HashSet Keywords = + new System.Collections.Generic.HashSet(StringComparer.OrdinalIgnoreCase) + { + "SELECT", "FROM", "WHERE", "AND", "OR", "NOT", "IN", "IS", "NULL", "AS", + "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "CROSS", "ON", "USING", + "GROUP", "BY", "ORDER", "HAVING", "LIMIT", "OFFSET", "DISTINCT", "ALL", + "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", "REPLACE", + "CREATE", "ALTER", "DROP", "TABLE", "INDEX", "VIEW", "TRIGGER", "IF", "EXISTS", + "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "DEFAULT", "CHECK", + "BEGIN", "COMMIT", "ROLLBACK", "TRANSACTION", "PRAGMA", "EXPLAIN", "WITH", + "CASE", "WHEN", "THEN", "ELSE", "END", "BETWEEN", "LIKE", "GLOB", "ESCAPE", + "UNION", "INTERSECT", "EXCEPT", "CAST", "COLLATE", "ASC", "DESC", + "COUNT", "SUM", "AVG", "MIN", "MAX", "IFNULL", "COALESCE", "NULLIF", + "LENGTH", "TRIM", "UPPER", "LOWER", "SUBSTR", "REPLACE", "ROUND", "ABS", + "DATETIME", "DATE", "TIME", "STRFTIME", "ADD", "COLUMN", "RENAME", "TO" + }; + + private const int WM_SETREDRAW = 0x000B; + + [DllImport("user32.dll")] + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + + public static void Apply(RichTextBox box, bool darkTheme) + { + if (box == null || box.IsDisposed) + { + return; + } + + string text = box.Text ?? string.Empty; + int selStart = box.SelectionStart; + int selLength = box.SelectionLength; + int firstVisibleChar = box.GetCharIndexFromPosition(new Point(1, 1)); + + Color defaultFg = darkTheme ? Color.FromArgb(220, 220, 220) : Color.FromArgb(30, 30, 30); + Color keyword = darkTheme ? Color.FromArgb(86, 156, 214) : Color.FromArgb(0, 0, 180); + Color str = darkTheme ? Color.FromArgb(206, 145, 120) : Color.FromArgb(163, 21, 21); + Color comment = darkTheme ? Color.FromArgb(106, 153, 85) : Color.FromArgb(0, 128, 0); + Color number = darkTheme ? Color.FromArgb(181, 206, 168) : Color.FromArgb(9, 134, 88); + Color param = darkTheme ? Color.FromArgb(197, 134, 192) : Color.FromArgb(128, 0, 128); + Color ident = darkTheme ? Color.FromArgb(156, 220, 254) : Color.FromArgb(0, 100, 120); + + SuspendRedraw(box); + try + { + box.SelectAll(); + box.SelectionColor = defaultFg; + box.SelectionFont = box.Font; + + foreach (Match match in TokenRegex.Matches(text)) + { + Color color = defaultFg; + bool bold = false; + + if (match.Groups[1].Success || match.Groups[2].Success) + { + color = comment; + } + else if (match.Groups[3].Success || match.Groups[4].Success || match.Groups[5].Success) + { + color = str; + } + else if (match.Groups[6].Success || match.Groups[7].Success) + { + color = param; + bold = true; + } + else if (match.Groups[8].Success) + { + color = number; + } + else if (match.Groups[9].Success) + { + var word = match.Groups[9].Value; + if (Keywords.Contains(word)) + { + color = keyword; + bold = true; + } + else + { + color = ident; + } + } + + if (color == defaultFg && !bold) + { + continue; + } + + box.Select(match.Index, match.Length); + box.SelectionColor = color; + if (bold) + { + box.SelectionFont = new Font(box.Font, FontStyle.Bold); + } + else + { + box.SelectionFont = box.Font; + } + } + } + finally + { + box.Select(selStart, selLength); + try + { + box.Select(firstVisibleChar, 0); + var pt = box.GetPositionFromCharIndex(firstVisibleChar); + if (pt.Y < 0) + { + // keep best-effort scroll + } + } + catch + { + // ignore scroll restore issues + } + + box.Select(selStart, selLength); + ResumeRedraw(box); + box.Invalidate(); + } + } + + private static void SuspendRedraw(Control control) + { + SendMessage(control.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero); + } + + private static void ResumeRedraw(Control control) + { + SendMessage(control.Handle, WM_SETREDRAW, new IntPtr(1), IntPtr.Zero); + } + } +} diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj index ba8999f..e670c27 100644 --- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj +++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj @@ -104,6 +104,8 @@ + + diff --git a/src/ElectronicPharmacy/Form1.UiShell.cs b/src/ElectronicPharmacy/Form1.UiShell.cs index 649dff5..4d06500 100644 --- a/src/ElectronicPharmacy/Form1.UiShell.cs +++ b/src/ElectronicPharmacy/Form1.UiShell.cs @@ -67,9 +67,9 @@ namespace Электронная_Фармация { Name = "statusFooter", Dock = DockStyle.Bottom, - Height = 30, + Height = 28, Tag = "status-footer", - Padding = new Padding(12, 0, 16, 0) + Padding = new Padding(10, 0, 4, 0) }; _statusFooter.Paint += (_, e) => { @@ -86,19 +86,27 @@ namespace Электронная_Фармация Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft, AutoEllipsis = true, - Tag = "muted" + Tag = "muted", + Padding = new Padding(0, 0, 4, 0) }; _btnSettings = new ModernButton { Name = "btnSettings", Dock = DockStyle.Right, - Width = 150, - Margin = new Padding(0), - Text = "Настройки" + Margin = new Padding(0, 2, 2, 2), + Text = "Настройки", + CompactIconMode = false, + AllowTextEllipsis = false, + MinimumSize = new Size(88, 22), + Size = new Size(88, 22), + Padding = new Padding(6, 0, 6, 0), + Font = new Font("Segoe UI Semibold", 8.5f, FontStyle.Bold), + TabIndex = 0 }; _btnSettings.Click += (_, __) => OpenSettingsDialog(); - _statusFooter.Controls.Add(_btnSettings); + // Сначала Fill (пользователь), потом Right (кнопка) — кнопка справа, текст не перекрывается. _statusFooter.Controls.Add(_lblCurrentUser); + _statusFooter.Controls.Add(_btnSettings); Controls.Add(_loadingOverlay); Controls.Add(_documentHost); @@ -111,6 +119,8 @@ namespace Электронная_Фармация contractorsItem.Click += (_, __) => tsItemContractors_Click(_, __); var consigneesItem = new ToolStripMenuItem("Грузополучатели"); consigneesItem.Click += (_, __) => tsConsignee_Click(_, __); + var switchConsigneeItem = new ToolStripMenuItem("Сменить грузополучателя"); + switchConsigneeItem.Click += (_, __) => SwitchConsignee(); var invoicesItem = new ToolStripMenuItem("Накладные"); invoicesItem.Click += (_, __) => tsItemInvoices_Click(_, __); var debugItem = new ToolStripMenuItem("Отладка"); @@ -119,6 +129,7 @@ namespace Электронная_Фармация { contractorsItem, consigneesItem, + switchConsigneeItem, invoicesItem, new ToolStripSeparator(), debugItem @@ -194,18 +205,38 @@ namespace Электронная_Фармация var login = Settings.Default.stringLogin?.Trim(); var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken); + var pharmacy = ConsigneeName?.Trim(); + if (string.IsNullOrWhiteSpace(pharmacy) || pharmacy == "Прайс-Лист") + { + pharmacy = ConsigneeHelper.GetDefaultConsigneeName(); + if (!string.IsNullOrWhiteSpace(pharmacy) && + (string.IsNullOrWhiteSpace(ConsigneeName) || ConsigneeName == "Прайс-Лист")) + { + ConsigneeName = pharmacy; + } + } + string userPart; if (!string.IsNullOrWhiteSpace(login) && hasToken) { - _lblCurrentUser.Text = $"Пользователь: {login}"; + userPart = $"Пользователь: {login}"; } else if (!string.IsNullOrWhiteSpace(login)) { - _lblCurrentUser.Text = $"Пользователь: {login} (не авторизован)"; + userPart = $"Пользователь: {login} (не авторизован)"; } else { - _lblCurrentUser.Text = "Пользователь не авторизован"; + userPart = "Пользователь не авторизован"; + } + + if (!string.IsNullOrWhiteSpace(pharmacy) && pharmacy != "Прайс-Лист") + { + _lblCurrentUser.Text = $"{userPart} | Аптека: {pharmacy}"; + } + else + { + _lblCurrentUser.Text = $"{userPart} | Аптека: не выбрана"; } } diff --git a/src/ElectronicPharmacy/Form1.cs b/src/ElectronicPharmacy/Form1.cs index c929301..9c49f08 100644 --- a/src/ElectronicPharmacy/Form1.cs +++ b/src/ElectronicPharmacy/Form1.cs @@ -74,17 +74,45 @@ namespace Электронная_Фармация UiThemeHelper.ApplyToControlTree(consigneeDialog); consigneeDialog.ShowDialog(); + if (string.IsNullOrWhiteSpace(ConsigneeName)) + { + var defaultName = ConsigneeHelper.GetDefaultConsigneeName(); + if (!string.IsNullOrWhiteSpace(defaultName)) + { + ConsigneeName = defaultName; + ConsigneeHelper.SetActiveConsignee(defaultName); + } + } + if (!HasDocumentTabs()) { loadDefaultPriceList(); } + + RefreshCurrentUserStatus(); } void loadDefaultPriceList() { - var ucPriceList = new UCPriceList("Прайс-Лист"); - OpenDocumentTab("Прайс-лист", "price", ucPriceList, allowDuplicate: true); + var consignee = !string.IsNullOrWhiteSpace(ConsigneeName) + ? ConsigneeName + : ConsigneeHelper.GetDefaultConsigneeName(); + if (string.IsNullOrWhiteSpace(consignee)) + { + consignee = "Прайс-Лист"; + } + else + { + ConsigneeName = consignee; + } + + var title = consignee == "Прайс-Лист" + ? "Прайс-лист" + : $"Прайс-лист ({consignee})"; + var ucPriceList = new UCPriceList(consignee); + OpenDocumentTab(title, "price", ucPriceList, allowDuplicate: true); ucPriceList.dgvPriceList.Focus(); + RefreshCurrentUserStatus(); } #region загрузкаПрайсЛиста_старая версия @@ -164,9 +192,23 @@ namespace Электронная_Фармация public void showMePriceList(string consignee) { ConsigneeName = consignee ?? string.Empty; + if (!string.IsNullOrWhiteSpace(ConsigneeName)) + { + ConsigneeHelper.SetActiveConsignee(ConsigneeName); + } + var ucPriceList = new UCPriceList(consignee); OpenDocumentTab($"Прайс-лист ({consignee})", "price", ucPriceList, allowDuplicate: true); ucPriceList.dgvPriceList.Focus(); + RefreshCurrentUserStatus(); + } + + private void SwitchConsignee() + { + var consigneeDialog = new HFConsignees { ParentForm = this }; + UiThemeHelper.ApplyToControlTree(consigneeDialog); + consigneeDialog.ShowDialog(this); + RefreshCurrentUserStatus(); } private void tsDebug_Click(object sender, EventArgs e) diff --git a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs index 289cfd6..cff0da6 100644 --- a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs +++ b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs @@ -76,7 +76,8 @@ namespace Электронная_Фармация.Forms [codeConsignees] integer not null, [ConsigneesName] nvarchar(128) not null, [ConsigneesAddress] nvarchar(256) not null, - [ConsigneesUseAsDefault] int(1) not null + [ConsigneesUseAsDefault] int(1) not null, + [LocationId] nvarchar(64) ) "; @@ -405,6 +406,8 @@ create table if not exists [OrderItems] ( TryAddColumn(conForCheckTables, "PriceList", "TradeName", "nvarchar(256)"); TryAddColumn(conForCheckTables, "PriceList", "Dosage", "nvarchar(128)"); TryAddColumn(conForCheckTables, "PriceList", "MarkupPercent", "real"); + TryAddColumn(conForCheckTables, "Consignees", "LocationId", "nvarchar(64)"); + ConsigneeHelper.MigrateGlobalLocationIdIfNeeded(conForCheckTables); } catch (Exception ex) { diff --git a/src/ElectronicPharmacy/Forms/FConsignees.Designer.cs b/src/ElectronicPharmacy/Forms/FConsignees.Designer.cs index f9e7bbd..0b58132 100644 --- a/src/ElectronicPharmacy/Forms/FConsignees.Designer.cs +++ b/src/ElectronicPharmacy/Forms/FConsignees.Designer.cs @@ -2,15 +2,8 @@ { partial class FConsignees { - /// - /// Required designer variable. - /// private System.ComponentModel.IContainer components = null; - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) @@ -22,13 +15,12 @@ #region Windows Form Designer generated code - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// private void InitializeComponent() { this.panel1 = new System.Windows.Forms.Panel(); + this.btnSetDefault = new Elfisa.UI.Controls.ModernButton(); + this.btnAdd = new Elfisa.UI.Controls.ModernButton(); + this.btnSave = new Elfisa.UI.Controls.ModernButton(); this.txtInteractiveShowMeConsignees = new Elfisa.UI.Controls.ModernTextBox(); this.label2 = new System.Windows.Forms.Label(); this.dgvConsignees = new Elfisa.UI.Controls.ModernDataGridView(); @@ -39,31 +31,67 @@ // panel1 // this.panel1.BackColor = System.Drawing.SystemColors.ButtonHighlight; + this.panel1.Controls.Add(this.btnSetDefault); + this.panel1.Controls.Add(this.btnAdd); + this.panel1.Controls.Add(this.btnSave); this.panel1.Controls.Add(this.txtInteractiveShowMeConsignees); this.panel1.Controls.Add(this.label2); this.panel1.Controls.Add(this.dgvConsignees); - this.panel1.Location = new System.Drawing.Point(13, 14); - this.panel1.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); + this.panel1.Dock = System.Windows.Forms.DockStyle.Fill; + this.panel1.Location = new System.Drawing.Point(0, 0); + this.panel1.Margin = new System.Windows.Forms.Padding(4); this.panel1.Name = "panel1"; - this.panel1.Size = new System.Drawing.Size(724, 423); + this.panel1.Padding = new System.Windows.Forms.Padding(12); + this.panel1.Size = new System.Drawing.Size(820, 480); this.panel1.TabIndex = 4; // + // btnSetDefault + // + this.btnSetDefault.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.btnSetDefault.Location = new System.Drawing.Point(280, 430); + this.btnSetDefault.Name = "btnSetDefault"; + this.btnSetDefault.Size = new System.Drawing.Size(180, 34); + this.btnSetDefault.TabIndex = 6; + this.btnSetDefault.Text = "По умолчанию"; + this.btnSetDefault.Click += new System.EventHandler(this.btnSetDefault_Click); + // + // btnAdd + // + this.btnAdd.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.btnAdd.Location = new System.Drawing.Point(148, 430); + this.btnAdd.Name = "btnAdd"; + this.btnAdd.Size = new System.Drawing.Size(120, 34); + this.btnAdd.TabIndex = 5; + this.btnAdd.Text = "Добавить"; + this.btnAdd.Click += new System.EventHandler(this.btnAdd_Click); + // + // btnSave + // + this.btnSave.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.btnSave.Location = new System.Drawing.Point(16, 430); + this.btnSave.Name = "btnSave"; + this.btnSave.Size = new System.Drawing.Size(120, 34); + this.btnSave.TabIndex = 4; + this.btnSave.Text = "Сохранить"; + this.btnSave.Click += new System.EventHandler(this.btnSave_Click); + // // txtInteractiveShowMeConsignees // - this.txtInteractiveShowMeConsignees.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204))); - this.txtInteractiveShowMeConsignees.Location = new System.Drawing.Point(91, 6); - this.txtInteractiveShowMeConsignees.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); + this.txtInteractiveShowMeConsignees.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtInteractiveShowMeConsignees.Font = new System.Drawing.Font("Segoe UI", 11.25F); + this.txtInteractiveShowMeConsignees.Location = new System.Drawing.Point(91, 16); + this.txtInteractiveShowMeConsignees.Margin = new System.Windows.Forms.Padding(4); this.txtInteractiveShowMeConsignees.Name = "txtInteractiveShowMeConsignees"; - this.txtInteractiveShowMeConsignees.Size = new System.Drawing.Size(623, 32); + this.txtInteractiveShowMeConsignees.Size = new System.Drawing.Size(700, 32); this.txtInteractiveShowMeConsignees.TabIndex = 3; this.txtInteractiveShowMeConsignees.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.txtInteractiveShowMeConsignees_KeyPress); // // label2 // this.label2.AutoSize = true; - this.label2.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204))); - this.label2.Location = new System.Drawing.Point(5, 6); - this.label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.label2.Font = new System.Drawing.Font("Segoe UI", 12F); + this.label2.Location = new System.Drawing.Point(16, 18); this.label2.Name = "label2"; this.label2.Size = new System.Drawing.Size(73, 28); this.label2.TabIndex = 2; @@ -73,45 +101,48 @@ // this.dgvConsignees.AllowUserToAddRows = false; this.dgvConsignees.AllowUserToDeleteRows = false; - this.dgvConsignees.AllowUserToResizeColumns = false; this.dgvConsignees.AllowUserToResizeRows = false; + this.dgvConsignees.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); this.dgvConsignees.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill; - this.dgvConsignees.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.DisplayedHeaders; this.dgvConsignees.BackgroundColor = System.Drawing.SystemColors.ButtonFace; this.dgvConsignees.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; - this.dgvConsignees.Location = new System.Drawing.Point(11, 44); - this.dgvConsignees.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); + this.dgvConsignees.Location = new System.Drawing.Point(16, 56); this.dgvConsignees.Name = "dgvConsignees"; - this.dgvConsignees.ReadOnly = true; + this.dgvConsignees.ReadOnly = false; this.dgvConsignees.RowHeadersWidthSizeMode = System.Windows.Forms.DataGridViewRowHeadersWidthSizeMode.AutoSizeToDisplayedHeaders; - this.dgvConsignees.Size = new System.Drawing.Size(704, 369); + this.dgvConsignees.Size = new System.Drawing.Size(775, 360); this.dgvConsignees.TabIndex = 1; // // FConsignees // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(749, 450); + this.ClientSize = new System.Drawing.Size(820, 480); this.Controls.Add(this.panel1); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; - this.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); - this.MaximizeBox = false; + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable; + this.MaximizeBox = true; this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(700, 420); this.Name = "FConsignees"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; - this.Text = "Грузополучатели"; + this.Text = "Грузополучатели (аптеки)"; this.Load += new System.EventHandler(this.FConsignees_Load); this.panel1.ResumeLayout(false); this.panel1.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.dgvConsignees)).EndInit(); this.ResumeLayout(false); - } #endregion + private System.Windows.Forms.Panel panel1; private Elfisa.UI.Controls.ModernTextBox txtInteractiveShowMeConsignees; private System.Windows.Forms.Label label2; private Elfisa.UI.Controls.ModernDataGridView dgvConsignees; + private Elfisa.UI.Controls.ModernButton btnSave; + private Elfisa.UI.Controls.ModernButton btnAdd; + private Elfisa.UI.Controls.ModernButton btnSetDefault; } -} \ No newline at end of file +} diff --git a/src/ElectronicPharmacy/Forms/FConsignees.cs b/src/ElectronicPharmacy/Forms/FConsignees.cs index 0f6efc8..8142cf1 100644 --- a/src/ElectronicPharmacy/Forms/FConsignees.cs +++ b/src/ElectronicPharmacy/Forms/FConsignees.cs @@ -1,14 +1,8 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Data.SQLite; using System.Windows.Forms; using Электронная_Фармация.Classes; -using System.Data.SQLite; namespace Электронная_Фармация.Forms { @@ -29,13 +23,15 @@ namespace Электронная_Фармация.Forms { using (SQLiteConnection conConsignees = new SQLiteConnection(AppConfig.SqliteConnectionString)) { - string selectFromConsignees = $@" -select [codeConsignees] as [Код грузополучателя], + string selectFromConsignees = @" +select [idConsignees] as [Id], +[codeConsignees] as [Код], [ConsigneesName] as [Наименование], [ConsigneesAddress] as [Адрес], -[ConsigneesUseAsDefault] as [По умолчанию?] +ifnull([LocationId], '') as [Location ID], +[ConsigneesUseAsDefault] as [По умолчанию] from [Consignees] -order by [ConsigneesUseAsDefault] desc"; +order by [ConsigneesUseAsDefault] desc, [ConsigneesName]"; try { conConsignees.Open(); @@ -44,16 +40,20 @@ order by [ConsigneesUseAsDefault] desc"; SQLiteDataAdapter daConsigneeList = new SQLiteDataAdapter(cmdShowConsigneesList); daConsigneeList.Fill(tableConsignees); dgvConsignees.DataSource = tableConsignees; - dgvConsignees.Columns[3].ValueType = typeof(float); + if (dgvConsignees.Columns.Contains("Id")) + { + dgvConsignees.Columns["Id"].Visible = false; + } + + foreach (DataGridViewColumn column in dgvConsignees.Columns) + { + column.ReadOnly = column.Name != "Location ID" && column.Name != "Адрес"; + } } catch (Exception ex) { MessageBox.Show($"Возникла ошибка при заполнении данных о грузополучателях.\nТекст ошибки:\n{ex}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); } - finally - { - conConsignees.Close(); - } } } @@ -61,9 +61,9 @@ order by [ConsigneesUseAsDefault] desc"; { if (e.KeyChar == (char)Keys.Enter) { - if (dgvConsignees.Rows.Count > 0) + if (dgvConsignees.Rows.Count > 0 && dgvConsignees.DataSource is DataTable table) { - (dgvConsignees.DataSource as DataTable).DefaultView.RowFilter = string.Format($"[Наименование] like '%{txtInteractiveShowMeConsignees.Text}%'"); + table.DefaultView.RowFilter = $"[Наименование] like '%{txtInteractiveShowMeConsignees.Text.Replace("'", "''")}%'"; if (dgvConsignees.Rows.Count == 0) { showMeContentFromConsigneesTable(); @@ -77,5 +77,133 @@ order by [ConsigneesUseAsDefault] desc"; } } + private void btnSave_Click(object sender, EventArgs e) + { + if (!(dgvConsignees.DataSource is DataTable table)) + { + return; + } + + dgvConsignees.EndEdit(); + + using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + connection.Open(); + using (var tx = connection.BeginTransaction()) + { + foreach (DataRow row in table.Rows) + { + if (row.RowState == DataRowState.Unchanged) + { + continue; + } + + using (var cmd = new SQLiteCommand( + @"UPDATE Consignees + SET ConsigneesAddress = @address, + LocationId = @locationId + WHERE idConsignees = @id", + connection, + tx)) + { + cmd.Parameters.AddWithValue("@address", row["Адрес"]?.ToString() ?? string.Empty); + cmd.Parameters.AddWithValue("@locationId", row["Location ID"]?.ToString()?.Trim() ?? string.Empty); + cmd.Parameters.AddWithValue("@id", Convert.ToInt64(row["Id"])); + cmd.ExecuteNonQuery(); + } + } + + tx.Commit(); + } + } + + table.AcceptChanges(); + ToastNotification.ShowSuccess("Грузополучатели сохранены"); + showMeContentFromConsigneesTable(); + } + + private void btnAdd_Click(object sender, EventArgs e) + { + using (var dialog = new Form()) + { + dialog.Text = "Новая аптека"; + dialog.FormBorderStyle = FormBorderStyle.FixedDialog; + dialog.StartPosition = FormStartPosition.CenterParent; + dialog.ClientSize = new System.Drawing.Size(420, 220); + dialog.MaximizeBox = false; + dialog.MinimizeBox = false; + + var lblName = new Label { Text = "Наименование", Left = 16, Top = 16, Width = 380 }; + var txtName = new TextBox { Left = 16, Top = 40, Width = 380 }; + var lblAddress = new Label { Text = "Адрес", Left = 16, Top = 76, Width = 380 }; + var txtAddress = new TextBox { Left = 16, Top = 100, Width = 380 }; + var lblLocation = new Label { Text = "Location ID", Left = 16, Top = 136, Width = 380 }; + var txtLocation = new TextBox { Left = 16, Top = 160, Width = 380 }; + var btnOk = new Button { Text = "Добавить", DialogResult = DialogResult.OK, Left = 220, Top = 188, Width = 80 }; + var btnCancel = new Button { Text = "Отмена", DialogResult = DialogResult.Cancel, Left = 310, Top = 188, Width = 80 }; + dialog.Controls.AddRange(new Control[] { lblName, txtName, lblAddress, txtAddress, lblLocation, txtLocation, btnOk, btnCancel }); + dialog.AcceptButton = btnOk; + dialog.CancelButton = btnCancel; + UiThemeHelper.ApplyToControlTree(dialog); + + if (dialog.ShowDialog(this) != DialogResult.OK) + { + return; + } + + var name = (txtName.Text ?? string.Empty).Trim(); + var address = (txtAddress.Text ?? string.Empty).Trim(); + var locationId = (txtLocation.Text ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(name)) + { + UiDialogs.ShowError("Укажите наименование аптеки.", "Грузополучатели", this); + return; + } + + using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + connection.Open(); + int nextCode = 1; + using (var codeCmd = new SQLiteCommand("SELECT ifnull(MAX(codeConsignees), 0) + 1 FROM Consignees", connection)) + { + nextCode = Convert.ToInt32(codeCmd.ExecuteScalar()); + } + + using (var insert = new SQLiteCommand( + @"INSERT INTO Consignees (codeConsignees, ConsigneesName, ConsigneesAddress, ConsigneesUseAsDefault, LocationId) + VALUES (@code, @name, @address, 0, @locationId)", + connection)) + { + insert.Parameters.AddWithValue("@code", nextCode); + insert.Parameters.AddWithValue("@name", name); + insert.Parameters.AddWithValue("@address", address); + insert.Parameters.AddWithValue("@locationId", locationId); + insert.ExecuteNonQuery(); + } + } + + ToastNotification.ShowSuccess("Аптека добавлена"); + showMeContentFromConsigneesTable(); + } + } + + private void btnSetDefault_Click(object sender, EventArgs e) + { + if (dgvConsignees.CurrentRow == null) + { + UiDialogs.ShowInfo("Выберите аптеку в списке.", "Грузополучатели", this); + return; + } + + var name = dgvConsignees.CurrentRow.Cells["Наименование"]?.Value?.ToString(); + if (string.IsNullOrWhiteSpace(name)) + { + return; + } + + ConsigneeHelper.SetActiveConsignee(name); + ToastNotification.ShowSuccess($"Аптека по умолчанию: {name}"); + showMeContentFromConsigneesTable(); + } } } diff --git a/src/ElectronicPharmacy/Forms/FDebug.Designer.cs b/src/ElectronicPharmacy/Forms/FDebug.Designer.cs index c00b120..c1f9252 100644 --- a/src/ElectronicPharmacy/Forms/FDebug.Designer.cs +++ b/src/ElectronicPharmacy/Forms/FDebug.Designer.cs @@ -2,15 +2,8 @@ { partial class FDebug { - /// - /// Required designer variable. - /// private System.ComponentModel.IContainer components = null; - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) @@ -22,115 +15,267 @@ #region Windows Form Designer generated code - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// private void InitializeComponent() { - this.RTXT_Sql = new System.Windows.Forms.RichTextBox(); - this.label1 = new System.Windows.Forms.Label(); - this.btnCleanSqlScript = new Elfisa.UI.Controls.ModernButton(); - this.btnExecuteSqlScript = new Elfisa.UI.Controls.ModernButton(); + this.splitMain = new System.Windows.Forms.SplitContainer(); + this.listTemplates = new System.Windows.Forms.ListBox(); + this.lblTemplates = new System.Windows.Forms.Label(); + this.splitRight = new System.Windows.Forms.SplitContainer(); + this.panelTop = new System.Windows.Forms.Panel(); + this.btnScanParams = new Elfisa.UI.Controls.ModernButton(); this.btnExecuteSelectSqlScript = new Elfisa.UI.Controls.ModernButton(); + this.btnExecuteSqlScript = new Elfisa.UI.Controls.ModernButton(); + this.btnCleanSqlScript = new Elfisa.UI.Controls.ModernButton(); + this.lblDbPath = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.RTXT_Sql = new System.Windows.Forms.RichTextBox(); + this.lblParams = new System.Windows.Forms.Label(); + this.panelParams = new System.Windows.Forms.Panel(); this.dgvSelectCommand = new Elfisa.UI.Controls.ModernDataGridView(); + this.lblStatus = new System.Windows.Forms.Label(); + ((System.ComponentModel.ISupportInitialize)(this.splitMain)).BeginInit(); + this.splitMain.Panel1.SuspendLayout(); + this.splitMain.Panel2.SuspendLayout(); + this.splitMain.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitRight)).BeginInit(); + this.splitRight.Panel1.SuspendLayout(); + this.splitRight.Panel2.SuspendLayout(); + this.splitRight.SuspendLayout(); + this.panelTop.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.dgvSelectCommand)).BeginInit(); this.SuspendLayout(); // - // RTXT_Sql + // splitMain // - this.RTXT_Sql.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.RTXT_Sql.Location = new System.Drawing.Point(9, 70); - this.RTXT_Sql.Margin = new System.Windows.Forms.Padding(4); - this.RTXT_Sql.Name = "RTXT_Sql"; - this.RTXT_Sql.Size = new System.Drawing.Size(565, 118); - this.RTXT_Sql.TabIndex = 0; - this.RTXT_Sql.Text = ""; + this.splitMain.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitMain.Location = new System.Drawing.Point(0, 0); + this.splitMain.Name = "splitMain"; // - // label1 + // splitMain.Panel1 // - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(6, 50); - this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(84, 16); - this.label1.TabIndex = 1; - this.label1.Text = "SQL-запрос"; + this.splitMain.Panel1.Controls.Add(this.listTemplates); + this.splitMain.Panel1.Controls.Add(this.lblTemplates); + this.splitMain.Panel1MinSize = 180; // - // btnCleanSqlScript + // splitMain.Panel2 // - this.btnCleanSqlScript.Location = new System.Drawing.Point(9, 15); - this.btnCleanSqlScript.Margin = new System.Windows.Forms.Padding(4); - this.btnCleanSqlScript.Name = "btnCleanSqlScript"; - this.btnCleanSqlScript.Size = new System.Drawing.Size(100, 28); - this.btnCleanSqlScript.TabIndex = 2; - this.btnCleanSqlScript.Text = "Очистить"; - this.btnCleanSqlScript.UseVisualStyleBackColor = true; - this.btnCleanSqlScript.Click += new System.EventHandler(this.btnCleanSqlScript_Click); + this.splitMain.Panel2.Controls.Add(this.splitRight); + this.splitMain.Size = new System.Drawing.Size(980, 620); + this.splitMain.SplitterDistance = 240; + this.splitMain.TabIndex = 0; // - // btnExecuteSqlScript + // listTemplates // - this.btnExecuteSqlScript.Location = new System.Drawing.Point(128, 15); - this.btnExecuteSqlScript.Margin = new System.Windows.Forms.Padding(4); - this.btnExecuteSqlScript.Name = "btnExecuteSqlScript"; - this.btnExecuteSqlScript.Size = new System.Drawing.Size(100, 28); - this.btnExecuteSqlScript.TabIndex = 3; - this.btnExecuteSqlScript.Text = "Выполнить"; - this.btnExecuteSqlScript.UseVisualStyleBackColor = true; - this.btnExecuteSqlScript.Click += new System.EventHandler(this.btnExecuteSqlScript_Click); + this.listTemplates.Dock = System.Windows.Forms.DockStyle.Fill; + this.listTemplates.Font = new System.Drawing.Font("Segoe UI", 10F); + this.listTemplates.FormattingEnabled = true; + this.listTemplates.ItemHeight = 23; + this.listTemplates.Location = new System.Drawing.Point(0, 28); + this.listTemplates.Name = "listTemplates"; + this.listTemplates.Size = new System.Drawing.Size(240, 592); + this.listTemplates.TabIndex = 1; + this.listTemplates.SelectedIndexChanged += new System.EventHandler(this.listTemplates_SelectedIndexChanged); + // + // lblTemplates + // + this.lblTemplates.Dock = System.Windows.Forms.DockStyle.Top; + this.lblTemplates.Font = new System.Drawing.Font("Segoe UI Semibold", 10F, System.Drawing.FontStyle.Bold); + this.lblTemplates.Location = new System.Drawing.Point(0, 0); + this.lblTemplates.Name = "lblTemplates"; + this.lblTemplates.Padding = new System.Windows.Forms.Padding(8, 4, 0, 0); + this.lblTemplates.Size = new System.Drawing.Size(240, 28); + this.lblTemplates.TabIndex = 0; + this.lblTemplates.Text = "Шаблоны SQL"; + // + // splitRight + // + this.splitRight.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitRight.Location = new System.Drawing.Point(0, 0); + this.splitRight.Name = "splitRight"; + this.splitRight.Orientation = System.Windows.Forms.Orientation.Horizontal; + // + // splitRight.Panel1 + // + this.splitRight.Panel1.Controls.Add(this.RTXT_Sql); + this.splitRight.Panel1.Controls.Add(this.panelParams); + this.splitRight.Panel1.Controls.Add(this.lblParams); + this.splitRight.Panel1.Controls.Add(this.panelTop); + this.splitRight.Panel1MinSize = 220; + // + // splitRight.Panel2 + // + this.splitRight.Panel2.Controls.Add(this.dgvSelectCommand); + this.splitRight.Panel2.Controls.Add(this.lblStatus); + this.splitRight.Panel2MinSize = 120; + this.splitRight.Size = new System.Drawing.Size(736, 620); + this.splitRight.SplitterDistance = 320; + this.splitRight.TabIndex = 0; + // + // panelTop + // + this.panelTop.Controls.Add(this.btnScanParams); + this.panelTop.Controls.Add(this.btnExecuteSelectSqlScript); + this.panelTop.Controls.Add(this.btnExecuteSqlScript); + this.panelTop.Controls.Add(this.btnCleanSqlScript); + this.panelTop.Controls.Add(this.lblDbPath); + this.panelTop.Controls.Add(this.label1); + this.panelTop.Dock = System.Windows.Forms.DockStyle.Top; + this.panelTop.Location = new System.Drawing.Point(0, 0); + this.panelTop.Name = "panelTop"; + this.panelTop.Size = new System.Drawing.Size(736, 72); + this.panelTop.TabIndex = 0; + // + // btnScanParams + // + this.btnScanParams.Location = new System.Drawing.Point(430, 36); + this.btnScanParams.Name = "btnScanParams"; + this.btnScanParams.Size = new System.Drawing.Size(150, 28); + this.btnScanParams.TabIndex = 5; + this.btnScanParams.Text = "Параметры из SQL"; + this.btnScanParams.Click += new System.EventHandler(this.btnScanParams_Click); // // btnExecuteSelectSqlScript // - this.btnExecuteSelectSqlScript.Location = new System.Drawing.Point(254, 15); - this.btnExecuteSelectSqlScript.Margin = new System.Windows.Forms.Padding(4); + this.btnExecuteSelectSqlScript.Location = new System.Drawing.Point(232, 36); this.btnExecuteSelectSqlScript.Name = "btnExecuteSelectSqlScript"; this.btnExecuteSelectSqlScript.Size = new System.Drawing.Size(182, 28); this.btnExecuteSelectSqlScript.TabIndex = 4; this.btnExecuteSelectSqlScript.Text = "Выполнить SELECT"; - this.btnExecuteSelectSqlScript.UseVisualStyleBackColor = true; this.btnExecuteSelectSqlScript.Click += new System.EventHandler(this.btnExecuteSelectSqlScript_Click); // + // btnExecuteSqlScript + // + this.btnExecuteSqlScript.Location = new System.Drawing.Point(116, 36); + this.btnExecuteSqlScript.Name = "btnExecuteSqlScript"; + this.btnExecuteSqlScript.Size = new System.Drawing.Size(100, 28); + this.btnExecuteSqlScript.TabIndex = 3; + this.btnExecuteSqlScript.Text = "Выполнить"; + this.btnExecuteSqlScript.Click += new System.EventHandler(this.btnExecuteSqlScript_Click); + // + // btnCleanSqlScript + // + this.btnCleanSqlScript.Location = new System.Drawing.Point(10, 36); + this.btnCleanSqlScript.Name = "btnCleanSqlScript"; + this.btnCleanSqlScript.Size = new System.Drawing.Size(100, 28); + this.btnCleanSqlScript.TabIndex = 2; + this.btnCleanSqlScript.Text = "Очистить"; + this.btnCleanSqlScript.Click += new System.EventHandler(this.btnCleanSqlScript_Click); + // + // lblDbPath + // + this.lblDbPath.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.lblDbPath.Location = new System.Drawing.Point(100, 8); + this.lblDbPath.Name = "lblDbPath"; + this.lblDbPath.Size = new System.Drawing.Size(620, 20); + this.lblDbPath.TabIndex = 1; + this.lblDbPath.Text = "db"; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(8, 8); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(84, 16); + this.label1.TabIndex = 0; + this.label1.Text = "SQL / база:"; + // + // RTXT_Sql + // + this.RTXT_Sql.Dock = System.Windows.Forms.DockStyle.Fill; + this.RTXT_Sql.Font = new System.Drawing.Font("Consolas", 10F); + this.RTXT_Sql.Location = new System.Drawing.Point(0, 100); + this.RTXT_Sql.Name = "RTXT_Sql"; + this.RTXT_Sql.Size = new System.Drawing.Size(736, 120); + this.RTXT_Sql.TabIndex = 3; + this.RTXT_Sql.Text = ""; + // + // lblParams + // + this.lblParams.Dock = System.Windows.Forms.DockStyle.Top; + this.lblParams.Location = new System.Drawing.Point(0, 72); + this.lblParams.Name = "lblParams"; + this.lblParams.Padding = new System.Windows.Forms.Padding(8, 4, 0, 0); + this.lblParams.Size = new System.Drawing.Size(736, 28); + this.lblParams.TabIndex = 1; + this.lblParams.Text = "Параметры (@name / {{name}})"; + // + // panelParams + // + this.panelParams.AutoScroll = true; + this.panelParams.Dock = System.Windows.Forms.DockStyle.Bottom; + this.panelParams.Location = new System.Drawing.Point(0, 220); + this.panelParams.Name = "panelParams"; + this.panelParams.Size = new System.Drawing.Size(736, 100); + this.panelParams.TabIndex = 2; + // // dgvSelectCommand // - this.dgvSelectCommand.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); + this.dgvSelectCommand.AllowUserToAddRows = false; + this.dgvSelectCommand.AllowUserToDeleteRows = false; + this.dgvSelectCommand.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.DisplayedCells; this.dgvSelectCommand.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; - this.dgvSelectCommand.Location = new System.Drawing.Point(9, 195); + this.dgvSelectCommand.Dock = System.Windows.Forms.DockStyle.Fill; + this.dgvSelectCommand.Location = new System.Drawing.Point(0, 0); this.dgvSelectCommand.Name = "dgvSelectCommand"; + this.dgvSelectCommand.ReadOnly = true; this.dgvSelectCommand.RowHeadersWidth = 51; this.dgvSelectCommand.RowTemplate.Height = 24; - this.dgvSelectCommand.Size = new System.Drawing.Size(562, 150); - this.dgvSelectCommand.TabIndex = 5; + this.dgvSelectCommand.Size = new System.Drawing.Size(736, 268); + this.dgvSelectCommand.TabIndex = 0; + // + // lblStatus + // + this.lblStatus.Dock = System.Windows.Forms.DockStyle.Bottom; + this.lblStatus.Location = new System.Drawing.Point(0, 268); + this.lblStatus.Name = "lblStatus"; + this.lblStatus.Padding = new System.Windows.Forms.Padding(8, 4, 8, 4); + this.lblStatus.Size = new System.Drawing.Size(736, 28); + this.lblStatus.TabIndex = 1; + this.lblStatus.Text = "Готово"; // // FDebug // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(583, 351); - this.Controls.Add(this.dgvSelectCommand); - this.Controls.Add(this.btnExecuteSelectSqlScript); - this.Controls.Add(this.btnExecuteSqlScript); - this.Controls.Add(this.btnCleanSqlScript); - this.Controls.Add(this.label1); - this.Controls.Add(this.RTXT_Sql); - this.Margin = new System.Windows.Forms.Padding(4); + this.ClientSize = new System.Drawing.Size(980, 620); + this.Controls.Add(this.splitMain); + this.MinimumSize = new System.Drawing.Size(800, 500); this.Name = "FDebug"; - this.Text = "FDebug"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "SQL-отладка"; + this.Load += new System.EventHandler(this.FDebug_Load); + this.splitMain.Panel1.ResumeLayout(false); + this.splitMain.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitMain)).EndInit(); + this.splitMain.ResumeLayout(false); + this.splitRight.Panel1.ResumeLayout(false); + this.splitRight.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitRight)).EndInit(); + this.splitRight.ResumeLayout(false); + this.panelTop.ResumeLayout(false); + this.panelTop.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.dgvSelectCommand)).EndInit(); this.ResumeLayout(false); - this.PerformLayout(); - } #endregion - private System.Windows.Forms.RichTextBox RTXT_Sql; - private System.Windows.Forms.Label label1; - private Elfisa.UI.Controls.ModernButton btnCleanSqlScript; - private Elfisa.UI.Controls.ModernButton btnExecuteSqlScript; + private System.Windows.Forms.SplitContainer splitMain; + private System.Windows.Forms.ListBox listTemplates; + private System.Windows.Forms.Label lblTemplates; + private System.Windows.Forms.SplitContainer splitRight; + private System.Windows.Forms.Panel panelTop; + private Elfisa.UI.Controls.ModernButton btnScanParams; private Elfisa.UI.Controls.ModernButton btnExecuteSelectSqlScript; + private Elfisa.UI.Controls.ModernButton btnExecuteSqlScript; + private Elfisa.UI.Controls.ModernButton btnCleanSqlScript; + private System.Windows.Forms.Label lblDbPath; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.RichTextBox RTXT_Sql; + private System.Windows.Forms.Label lblParams; + private System.Windows.Forms.Panel panelParams; private Elfisa.UI.Controls.ModernDataGridView dgvSelectCommand; + private System.Windows.Forms.Label lblStatus; } -} \ No newline at end of file +} diff --git a/src/ElectronicPharmacy/Forms/FDebug.cs b/src/ElectronicPharmacy/Forms/FDebug.cs index bf8022a..8892f58 100644 --- a/src/ElectronicPharmacy/Forms/FDebug.cs +++ b/src/ElectronicPharmacy/Forms/FDebug.cs @@ -1,81 +1,385 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Data; +using System.Data.SQLite; +using System.Diagnostics; using System.Drawing; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Text.RegularExpressions; using System.Windows.Forms; +using Elfisa.UI.Theming; using Электронная_Фармация.Classes; -using System.Data.SQLite; namespace Электронная_Фармация.Forms { public partial class FDebug : Form { + private sealed class SqlTemplate + { + public string Title { get; set; } + public string Sql { get; set; } + public string[] Parameters { get; set; } + } + + private readonly List _templates = new List(); + private readonly Dictionary _paramBoxes = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Timer _highlightTimer = new Timer(); + private bool _highlighting; + public FDebug() { InitializeComponent(); + _highlightTimer.Interval = 180; + _highlightTimer.Tick += (_, __) => + { + _highlightTimer.Stop(); + ApplySqlHighlight(); + }; } - private void btnExecuteSqlScript_Click(object sender, EventArgs e) + private void FDebug_Load(object sender, EventArgs e) { - string connectionStringToLocalDB = AppConfig.SqliteConnectionString; - - string sqlScript = RTXT_Sql.Text; - - - using (SQLiteConnection debugCon = new SQLiteConnection(connectionStringToLocalDB)) + UiThemeHelper.ApplyToControlTree(this); + Text = "SQL-отладка"; + lblDbPath.Text = AppConfig.SqliteDbPath; + RTXT_Sql.DetectUrls = false; + RTXT_Sql.AcceptsTab = true; + RTXT_Sql.TextChanged += RTXT_Sql_TextChanged; + BuildTemplates(); + listTemplates.Items.Clear(); + foreach (var template in _templates) { - try + listTemplates.Items.Add(template.Title); + } + + SetStatus("Готово. Выберите шаблон или введите свой SQL."); + ApplySqlHighlight(); + FormClosed += (_, __) => + { + _highlightTimer.Stop(); + _highlightTimer.Dispose(); + }; + } + + private void RTXT_Sql_TextChanged(object sender, EventArgs e) + { + if (_highlighting) + { + return; + } + + _highlightTimer.Stop(); + _highlightTimer.Start(); + } + + private void ApplySqlHighlight() + { + if (_highlighting || RTXT_Sql == null || RTXT_Sql.IsDisposed) + { + return; + } + + _highlighting = true; + try + { + SqlSyntaxHighlighter.Apply(RTXT_Sql, ThemeManager.IsDarkProductivityActive); + } + finally + { + _highlighting = false; + } + } + + private void BuildTemplates() + { + _templates.Clear(); + _templates.Add(new SqlTemplate + { + Title = "Счётчик прайса", + Sql = "SELECT COUNT(*) AS [Позиций] FROM PriceList;" + }); + _templates.Add(new SqlTemplate + { + Title = "Прайс по поставщикам", + Sql = @"SELECT SupplierName AS [Поставщик], + COUNT(*) AS [Позиций], + MIN(Price) AS [МинЦена], + MAX(Price) AS [МаксЦена] +FROM PriceList +GROUP BY SupplierName +ORDER BY COUNT(*) DESC;" + }); + _templates.Add(new SqlTemplate + { + Title = "Заказы по статусам", + Sql = @"SELECT OrderState AS [Статус], COUNT(*) AS [Кол-во] +FROM Orders +GROUP BY OrderState +ORDER BY COUNT(*) DESC;" + }); + _templates.Add(new SqlTemplate + { + Title = "Грузополучатели + LocationId", + Sql = @"SELECT codeConsignees AS [Код], + ConsigneesName AS [Аптека], + ConsigneesAddress AS [Адрес], + ifnull(LocationId, '') AS [LocationId], + ConsigneesUseAsDefault AS [ПоУмолчанию] +FROM Consignees +ORDER BY ConsigneesUseAsDefault DESC, ConsigneesName;" + }); + _templates.Add(new SqlTemplate + { + Title = "Корзина TempOrderItems", + Sql = @"SELECT COUNT(*) AS [Строк], + ifnull(SUM(CAST(SummaZakaza AS REAL)), 0) AS [Сумма] +FROM TempOrderItems;" + }); + _templates.Add(new SqlTemplate + { + Title = "Поиск по наименованию", + Sql = @"SELECT DrugName, SupplierName, Price, Quantity, supplier_price_id +FROM PriceList +WHERE DrugName LIKE '%' || @q || '%' +ORDER BY DrugName +LIMIT 200;", + Parameters = new[] { "q" } + }); + _templates.Add(new SqlTemplate + { + Title = "Последние заказы", + Sql = @"SELECT Id_Order, OrderNumber, OrderDate, SupplierName, ConsigneeName, OrderState, SummaZakaza +FROM Orders +ORDER BY Id_Order DESC +LIMIT 50;" + }); + _templates.Add(new SqlTemplate + { + Title = "Список таблиц", + Sql = @"SELECT name AS [Таблица], type AS [Тип] +FROM sqlite_master +WHERE type IN ('table', 'view') +ORDER BY name;" + }); + _templates.Add(new SqlTemplate + { + Title = "Позиции заказа", + Sql = @"SELECT oi.Id_Order, oi.DrugName, oi.Zakaz, oi.Price, oi.supplier_price_id, o.ConsigneeName, o.OrderState +FROM OrderItems oi +INNER JOIN Orders o ON o.Id_Order = oi.Id_Order +WHERE oi.Id_Order = @orderId +ORDER BY oi.DrugName;", + Parameters = new[] { "orderId" } + }); + _templates.Add(new SqlTemplate + { + Title = "Очистить корзину (DML)", + Sql = "DELETE FROM TempOrderItems;" + }); + } + + private void listTemplates_SelectedIndexChanged(object sender, EventArgs e) + { + if (listTemplates.SelectedIndex < 0 || listTemplates.SelectedIndex >= _templates.Count) + { + return; + } + + var template = _templates[listTemplates.SelectedIndex]; + RTXT_Sql.Text = template.Sql; + RebuildParamEditors(template.Parameters); + SetStatus($"Шаблон: {template.Title}"); + } + + private void RebuildParamEditors(string[] parameters) + { + panelParams.Controls.Clear(); + _paramBoxes.Clear(); + + if (parameters == null || parameters.Length == 0) + { + var empty = new Label { - debugCon.Open(); - SQLiteCommand cmdDebug = new SQLiteCommand(sqlScript, debugCon); - cmdDebug.ExecuteNonQuery(); - } - catch (Exception ex) + Text = "Параметры не требуются. Можно писать {{name}} или @name в SQL.", + AutoSize = true, + MaximumSize = new Size(panelParams.Width - 20, 0), + Location = new Point(8, 8) + }; + panelParams.Controls.Add(empty); + return; + } + + int y = 8; + foreach (var name in parameters) + { + var lbl = new Label { - MessageBox.Show($"При выполнении запроса возникла ошибка. Текст ошибки:\n{ex.ToString()}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - finally + Text = "@" + name, + AutoSize = true, + Location = new Point(8, y + 4) + }; + var box = new TextBox { - debugCon.Close(); - } + Name = "param_" + name, + Width = Math.Max(180, panelParams.Width - 140), + Location = new Point(120, y) + }; + panelParams.Controls.Add(lbl); + panelParams.Controls.Add(box); + _paramBoxes[name] = box; + y += 34; } } private void btnCleanSqlScript_Click(object sender, EventArgs e) { - RTXT_Sql.Text = ""; + RTXT_Sql.Text = string.Empty; + dgvSelectCommand.DataSource = null; + RebuildParamEditors(null); + SetStatus("Очищено."); + } + + private void btnExecuteSqlScript_Click(object sender, EventArgs e) + { + ExecuteSql(forceSelect: false); } private void btnExecuteSelectSqlScript_Click(object sender, EventArgs e) { - string connectionStringToLocalDB = AppConfig.SqliteConnectionString; + ExecuteSql(forceSelect: true); + } - string sqlScript = RTXT_Sql.Text; + private void ExecuteSql(bool forceSelect) + { + var sql = (RTXT_Sql.Text ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(sql)) + { + SetStatus("Пустой SQL."); + return; + } - using (SQLiteConnection debugSelectCon = new SQLiteConnection(connectionStringToLocalDB)) + sql = ApplyMustachePlaceholders(sql); + bool isSelect = forceSelect || LooksLikeSelect(sql); + var sw = Stopwatch.StartNew(); + + using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) { try { - debugSelectCon.Open(); - SQLiteCommand cmdSelectSqlCommand = new SQLiteCommand(sqlScript, debugSelectCon); - SQLiteDataAdapter daSelectDataAdapter = new SQLiteDataAdapter(cmdSelectSqlCommand); - DataTable tableSelect = new DataTable(); - daSelectDataAdapter.Fill(tableSelect); - dgvSelectCommand.DataSource = tableSelect; + connection.Open(); + using (var command = new SQLiteCommand(sql, connection)) + { + BindParameters(command, sql); + + if (isSelect) + { + using (var adapter = new SQLiteDataAdapter(command)) + { + var table = new DataTable(); + adapter.Fill(table); + dgvSelectCommand.DataSource = table; + sw.Stop(); + SetStatus($"SELECT: {table.Rows.Count} строк за {sw.ElapsedMilliseconds} мс"); + } + } + else + { + int affected = command.ExecuteNonQuery(); + dgvSelectCommand.DataSource = null; + sw.Stop(); + SetStatus($"Выполнено. Затронуто строк: {affected}. {sw.ElapsedMilliseconds} мс"); + } + } } - catch(Exception ex) + catch (Exception ex) { - MessageBox.Show($"Возникла ошибка при заполнении таблицы данными.\nТекст ошибки:\n{ex.ToString()}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - finally - { - debugSelectCon.Close(); + sw.Stop(); + SetStatus("Ошибка: " + ex.Message); + MessageBox.Show($"При выполнении запроса возникла ошибка.\n{ex}", "SQL-отладка", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } + + private static bool LooksLikeSelect(string sql) + { + var trimmed = sql.TrimStart(); + return trimmed.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase) + || trimmed.StartsWith("PRAGMA", StringComparison.OrdinalIgnoreCase) + || trimmed.StartsWith("WITH", StringComparison.OrdinalIgnoreCase) + || trimmed.StartsWith("EXPLAIN", StringComparison.OrdinalIgnoreCase); + } + + private string ApplyMustachePlaceholders(string sql) + { + return Regex.Replace(sql, @"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}", match => + { + var name = match.Groups[1].Value; + if (_paramBoxes.TryGetValue(name, out var box)) + { + return (box.Text ?? string.Empty).Replace("'", "''"); + } + + return match.Value; + }); + } + + private void BindParameters(SQLiteCommand command, string sql) + { + var names = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (Match match in Regex.Matches(sql, @"@([A-Za-z_][A-Za-z0-9_]*)")) + { + names.Add(match.Groups[1].Value); + } + + foreach (var name in names) + { + object value = string.Empty; + if (_paramBoxes.TryGetValue(name, out var box)) + { + value = box.Text ?? string.Empty; + } + + command.Parameters.AddWithValue("@" + name, value); + } + + // Параметры из панели, даже если в SQL их ещё нет (удобно для шаблонов). + foreach (var pair in _paramBoxes) + { + if (!names.Contains(pair.Key)) + { + command.Parameters.AddWithValue("@" + pair.Key, pair.Value.Text ?? string.Empty); + } + } + } + + private void SetStatus(string text) + { + lblStatus.Text = text; + } + + private void btnScanParams_Click(object sender, EventArgs e) + { + var sql = RTXT_Sql.Text ?? string.Empty; + var names = Regex.Matches(sql, @"@([A-Za-z_][A-Za-z0-9_]*)") + .Cast() + .Select(m => m.Groups[1].Value) + .Concat(Regex.Matches(sql, @"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}") + .Cast() + .Select(m => m.Groups[1].Value)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + var existing = _paramBoxes.ToDictionary(p => p.Key, p => p.Value.Text, StringComparer.OrdinalIgnoreCase); + RebuildParamEditors(names); + foreach (var pair in existing) + { + if (_paramBoxes.TryGetValue(pair.Key, out var box)) + { + box.Text = pair.Value; + } + } + + SetStatus(names.Length == 0 ? "Параметры в SQL не найдены." : $"Найдено параметров: {names.Length}"); + } } } diff --git a/src/ElectronicPharmacy/HelpForms/HFConsignees.cs b/src/ElectronicPharmacy/HelpForms/HFConsignees.cs index 04fcd84..6d719c7 100644 --- a/src/ElectronicPharmacy/HelpForms/HFConsignees.cs +++ b/src/ElectronicPharmacy/HelpForms/HFConsignees.cs @@ -1,21 +1,16 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; -using Электронная_Фармация.Classes; using System.Data.SQLite; +using System.Windows.Forms; using Электронная_Фармация; +using Электронная_Фармация.Classes; namespace Электронная_Фармация.HelpForms { public partial class HFConsignees : Form { - public ElectroPharmacy ParentForm { get; set; } + public new ElectroPharmacy ParentForm { get; set; } + public HFConsignees() { InitializeComponent(); @@ -31,7 +26,12 @@ namespace Электронная_Фармация.HelpForms { conConsigneesList.Open(); - string qShowMeConsigneesList = "select ConsigneesName as [Грузополучатель] from Consignees order by ConsigneesUseAsDefault desc"; + string qShowMeConsigneesList = @" +select ConsigneesName as [Грузополучатель], + ifnull(LocationId, '') as [Location ID], + ConsigneesAddress as [Адрес] +from Consignees +order by ConsigneesUseAsDefault desc, ConsigneesName"; SQLiteCommand cmdShowMeConsigneesList = new SQLiteCommand(qShowMeConsigneesList, conConsigneesList); SQLiteDataAdapter daConsigneesList = new SQLiteDataAdapter(cmdShowMeConsigneesList); @@ -43,16 +43,13 @@ namespace Электронная_Фармация.HelpForms { UiDialogs.ShowError($"При загрузке списка получателей возникла ошибка.\n{ex}", "Ошибка", this); } - finally - { - conConsigneesList.Close(); - } } } private void btnCloseThis_Click(object sender, EventArgs e) { - this.Close(); + DialogResult = DialogResult.Cancel; + Close(); } private void btnConfirmConsignee_Click(object sender, EventArgs e) @@ -63,7 +60,27 @@ namespace Электронная_Фармация.HelpForms return; } - string consigneesName = dgvConsignees.CurrentRow.Cells[0].Value.ToString(); + string consigneesName = dgvConsignees.CurrentRow.Cells[0].Value?.ToString(); + if (string.IsNullOrWhiteSpace(consigneesName)) + { + UiDialogs.ShowInfo("Выберите грузополучателя из списка.", "Грузополучатель", this); + return; + } + + var locationId = ConsigneeHelper.GetLocationIdByName(consigneesName); + if (string.IsNullOrWhiteSpace(locationId)) + { + var ask = UiDialogs.ConfirmYesNo( + $"У аптеки «{consigneesName}» не задан Location ID.\nПродолжить без него? Заказы этой аптеки нельзя будет отправить, пока ID не заполните.", + "Location ID", + this); + if (ask != DialogResult.Yes) + { + return; + } + } + + ConsigneeHelper.SetActiveConsignee(consigneesName); ParentForm?.showMePriceList(consigneesName); DialogResult = DialogResult.OK; Close(); diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs index 521aa05..a9f8ed2 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs @@ -149,68 +149,202 @@ namespace Электронная_Фармация.HelpForms ? null : Settings.Default.RegionId; - // Один крупный запрос (OFFSET на больших страницах валит сервер). - const int pageSize = 200000; - const int maxPages = 100; + const int supplierPageLimit = 100000; + const int discoveryLimit = 50000; var combined = new PriceSummaryResponse { Summary = new List() }; var transferProgress = new Progress(ReportTransferProgress); - for (int page = 0; page < maxPages; page++) + var supplierIds = new List(ConsigneeHelper.GetKnownSupplierIds()); + if (supplierIds.Count == 0) { - int offset = page * pageSize; + AppendStatus("Первая загрузка: определяю список поставщиков..."); + SetOverallProgress(3, "Определение поставщиков..."); + var discovery = await GetPriceSummaryWithAuthRetryAsync( + client, + supplierId: null, + regionId: regionId, + limit: discoveryLimit, + offset: 0, + transferProgress: transferProgress); + + if (discovery?.MarkupPercent.HasValue == true) + { + combined.MarkupPercent = discovery.MarkupPercent; + } + + var discovered = new HashSet(StringComparer.OrdinalIgnoreCase); + if (discovery?.Summary != null) + { + foreach (var item in discovery.Summary) + { + if (!string.IsNullOrWhiteSpace(item.SupplierId)) + { + discovered.Add(item.SupplierId.Trim()); + } + } + } + + supplierIds.AddRange(discovered); + AppendStatus($"Найдено поставщиков: {supplierIds.Count}"); + } + + if (supplierIds.Count == 0) + { + AppendStatus("Поставщики не найдены, пробую полный запрос..."); + var fallback = await GetPriceSummaryWithAuthRetryAsync( + client, + supplierId: null, + regionId: regionId, + limit: 200000, + offset: 0, + transferProgress: transferProgress); + if (fallback?.MarkupPercent.HasValue == true) + { + combined.MarkupPercent = fallback.MarkupPercent; + } + + if (fallback?.Summary != null) + { + combined.Summary.AddRange(fallback.Summary); + } + + return combined; + } + + int totalSuppliers = supplierIds.Count; + for (int i = 0; i < totalSuppliers; i++) + { + var supplierId = supplierIds[i]; + int supplierProgressStart = 3 + (int)((DownloadPhaseEnd - 3) * ((double)i / totalSuppliers)); + int supplierProgressEnd = 3 + (int)((DownloadPhaseEnd - 3) * ((double)(i + 1) / totalSuppliers)); SetOverallProgress( - Math.Min(DownloadPhaseEnd - 1, 3 + page), - page == 0 ? "Скачивание прайса с сервера..." : $"Скачивание страницы {page + 1}..."); + supplierProgressStart, + $"Поставщик {i + 1}/{totalSuppliers}..."); + AppendStatus($"Загрузка поставщика {i + 1}/{totalSuppliers}: {supplierId}"); PriceSummaryResponse chunk; try { - chunk = await client.GetPriceSummaryAsync( - supplierId: null, + chunk = await GetPriceSummaryWithAuthRetryAsync( + client, + supplierId: supplierId, regionId: regionId, - limit: pageSize, - offset: offset, + limit: supplierPageLimit, + offset: 0, transferProgress: transferProgress); } - catch (UnauthorizedAccessException) + catch (Exception ex) { - AppendStatus("Токен недействителен, повторная авторизация..."); - SetOverallProgress(2, "Повторная авторизация..."); - Settings.Default.stringToken = string.Empty; - Settings.Default.Save(); - client.SetToken(null); - await LoginAndSaveTokenAsync(client); - SetOverallProgress(3, "Повторная загрузка прайса..."); - chunk = await client.GetPriceSummaryAsync( - supplierId: null, - regionId: regionId, - limit: pageSize, - offset: offset, - transferProgress: transferProgress); + AppendStatus($"Ошибка поставщика {supplierId}: {ex.Message}"); + AppDebugLog.Error("Download", $"Ошибка загрузки прайса поставщика {supplierId}", ex); + continue; } - int got = chunk?.Summary?.Count ?? 0; - if (page == 0 && chunk?.MarkupPercent.HasValue == true) + if (i == 0 && chunk?.MarkupPercent.HasValue == true && !combined.MarkupPercent.HasValue) { combined.MarkupPercent = chunk.MarkupPercent; } + int got = chunk?.Summary?.Count ?? 0; if (got > 0) { combined.Summary.AddRange(chunk.Summary); - AppendStatus($"Загружено {combined.Summary.Count} позиций..."); } - if (got < pageSize) + SetOverallProgress( + Math.Max(supplierProgressStart, supplierProgressEnd - 1), + $"Поставщик {i + 1}/{totalSuppliers}: +{got}, всего {combined.Summary.Count}"); + AppendStatus($"Поставщик {i + 1}/{totalSuppliers}: получено {got}, всего {combined.Summary.Count}"); + } + + // Лёгкий discovery новых поставщиков, которых ещё не было в локальной базе. + try + { + AppendStatus("Проверка новых поставщиков..."); + var probe = await GetPriceSummaryWithAuthRetryAsync( + client, + supplierId: null, + regionId: regionId, + limit: 5000, + offset: 0, + transferProgress: null); + var known = new HashSet(supplierIds, StringComparer.OrdinalIgnoreCase); + var newcomers = new List(); + if (probe?.Summary != null) { - break; + foreach (var item in probe.Summary) + { + if (!string.IsNullOrWhiteSpace(item.SupplierId) && known.Add(item.SupplierId.Trim())) + { + newcomers.Add(item.SupplierId.Trim()); + } + } } + + for (int i = 0; i < newcomers.Count; i++) + { + var supplierId = newcomers[i]; + AppendStatus($"Новый поставщик {i + 1}/{newcomers.Count}: {supplierId}"); + var chunk = await GetPriceSummaryWithAuthRetryAsync( + client, + supplierId: supplierId, + regionId: regionId, + limit: supplierPageLimit, + offset: 0, + transferProgress: transferProgress); + int got = chunk?.Summary?.Count ?? 0; + if (got > 0) + { + combined.Summary.AddRange(chunk.Summary); + } + + AppendStatus($"Новый поставщик: +{got}, всего {combined.Summary.Count}"); + } + } + catch (Exception ex) + { + AppendStatus($"Проверка новых поставщиков пропущена: {ex.Message}"); + AppDebugLog.Error("Download", "Ошибка discovery новых поставщиков", ex); } return combined; } + private async Task GetPriceSummaryWithAuthRetryAsync( + ApiClient client, + string supplierId, + string regionId, + int limit, + int offset, + IProgress transferProgress) + { + try + { + return await client.GetPriceSummaryAsync( + supplierId: supplierId, + regionId: regionId, + limit: limit, + offset: offset, + transferProgress: transferProgress); + } + catch (UnauthorizedAccessException) + { + AppendStatus("Токен недействителен, повторная авторизация..."); + SetOverallProgress(2, "Повторная авторизация..."); + Settings.Default.stringToken = string.Empty; + Settings.Default.Save(); + client.SetToken(null); + await LoginAndSaveTokenAsync(client); + return await client.GetPriceSummaryAsync( + supplierId: supplierId, + regionId: regionId, + limit: limit, + offset: offset, + transferProgress: transferProgress); + } + } + private static void ApplyClientMarkupSettings(PriceSummaryResponse response) { if (response?.MarkupPercent == null) @@ -379,29 +513,47 @@ VALUES } using (var transaction = sqliteCon.BeginTransaction()) + using (var cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction)) { + var pGuidEs = cmd.Parameters.Add("@guid_es", DbType.String); + var pEsCode = cmd.Parameters.Add("@es_code", DbType.String); + var pSupplierId = cmd.Parameters.Add("@supplier_id", DbType.String); + var pDrugName = cmd.Parameters.Add("@DrugName", DbType.String); + var pSupplierName = cmd.Parameters.Add("@SupplierName", DbType.String); + var pPrice = cmd.Parameters.Add("@Price", DbType.String); + var pQuantity = cmd.Parameters.Add("@Quantity", DbType.String); + var pExpiry = cmd.Parameters.Add("@ExpiryPeriod", DbType.String); + var pDescription = cmd.Parameters.Add("@Description", DbType.String); + var pZakaz = cmd.Parameters.Add("@Zakaz", DbType.String); + var pSumma = cmd.Parameters.Add("@SummaZakaza", DbType.String); + var pSupplierPriceId = cmd.Parameters.Add("@supplier_price_id", DbType.String); + var pTradeName = cmd.Parameters.Add("@TradeName", DbType.String); + var pDosage = cmd.Parameters.Add("@Dosage", DbType.String); + var pMarkup = cmd.Parameters.Add("@MarkupPercent", DbType.Object); + int saved = 0; + bool hasTradeName = tablePrice.Columns.Contains("TradeName"); + bool hasDosage = tablePrice.Columns.Contains("Dosage"); + bool hasMarkup = tablePrice.Columns.Contains("MarkupPercent"); + foreach (DataRow row in tablePrice.Rows) { - using (var cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction)) - { - cmd.Parameters.AddWithValue("@guid_es", row["guid_es"]); - cmd.Parameters.AddWithValue("@es_code", row["es_code"]); - cmd.Parameters.AddWithValue("@supplier_id", row["supplier_id"]); - cmd.Parameters.AddWithValue("@DrugName", row["DrugName"]); - cmd.Parameters.AddWithValue("@SupplierName", row["SupplierName"]); - cmd.Parameters.AddWithValue("@Price", row["Price"]); - cmd.Parameters.AddWithValue("@Quantity", row["Quantity"]); - cmd.Parameters.AddWithValue("@ExpiryPeriod", row["ExpiryPeriod"]); - cmd.Parameters.AddWithValue("@Description", row["Description"]); - cmd.Parameters.AddWithValue("@Zakaz", row["Zakaz"]); - cmd.Parameters.AddWithValue("@SummaZakaza", row["SummaZakaza"]); - cmd.Parameters.AddWithValue("@supplier_price_id", row["supplier_price_id"]); - cmd.Parameters.AddWithValue("@TradeName", row.Table.Columns.Contains("TradeName") ? row["TradeName"] : DBNull.Value); - cmd.Parameters.AddWithValue("@Dosage", row.Table.Columns.Contains("Dosage") ? row["Dosage"] : DBNull.Value); - cmd.Parameters.AddWithValue("@MarkupPercent", row.Table.Columns.Contains("MarkupPercent") ? row["MarkupPercent"] : DBNull.Value); - cmd.ExecuteNonQuery(); - } + pGuidEs.Value = row["guid_es"] ?? DBNull.Value; + pEsCode.Value = row["es_code"] ?? DBNull.Value; + pSupplierId.Value = row["supplier_id"] ?? DBNull.Value; + pDrugName.Value = row["DrugName"] ?? DBNull.Value; + pSupplierName.Value = row["SupplierName"] ?? DBNull.Value; + pPrice.Value = row["Price"] ?? DBNull.Value; + pQuantity.Value = row["Quantity"] ?? DBNull.Value; + pExpiry.Value = row["ExpiryPeriod"] ?? DBNull.Value; + pDescription.Value = row["Description"] ?? DBNull.Value; + pZakaz.Value = row["Zakaz"] ?? DBNull.Value; + pSumma.Value = row["SummaZakaza"] ?? DBNull.Value; + pSupplierPriceId.Value = row["supplier_price_id"] ?? DBNull.Value; + pTradeName.Value = hasTradeName ? (row["TradeName"] ?? DBNull.Value) : DBNull.Value; + pDosage.Value = hasDosage ? (row["Dosage"] ?? DBNull.Value) : DBNull.Value; + pMarkup.Value = hasMarkup ? (row["MarkupPercent"] ?? DBNull.Value) : DBNull.Value; + cmd.ExecuteNonQuery(); saved++; if (progress != null && (saved % 250 == 0 || saved == total)) diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs index cf06cb1..3185a7f 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs @@ -16,10 +16,6 @@ namespace Электронная_Фармация.HelpForms Shown += (_, __) => WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors); } - string login = Settings.Default.stringLogin.ToString(); - string password = Settings.Default.stringPassword.ToString(); - string doWeHaveAToken = Settings.Default.stringToken.ToString(); - private void btnCloseThis_Click(object sender, EventArgs e) { Close(); @@ -30,8 +26,8 @@ namespace Электронная_Фармация.HelpForms btnConfirmRegistration.Enabled = false; btnCloseThis.Enabled = false; UseWaitCursor = true; - string strLogin = txtLogin.Text; - string strPassword = txtPassword.Text; + string strLogin = (txtLogin.Text ?? string.Empty).Trim(); + string strPassword = txtPassword.Text ?? string.Empty; if (strLogin.Length > 0 && strPassword.Length > 0) { @@ -40,7 +36,7 @@ namespace Электронная_Фармация.HelpForms : Settings.Default.ApiBaseUrl; LoginRequest loginRequest = new LoginRequest(); - loginRequest.Username = strLogin.Trim(); + loginRequest.Username = strLogin; loginRequest.Password = strPassword.Trim(); var client = new ApiClient(AppConfig.NormalizeApiBaseUrl(apiUrl)); @@ -50,8 +46,9 @@ namespace Электронная_Фармация.HelpForms { string token = await client.LoginAsync(loginRequest.Username, loginRequest.Password); + // Сохраняем только логин и токен; пароль в настройках не храним. Settings.Default.stringLogin = strLogin; - Settings.Default.stringPassword = strPassword; + Settings.Default.stringPassword = string.Empty; Settings.Default.stringToken = token; Settings.Default.ApiBaseUrl = client.BaseUrl; Settings.Default.Save(); @@ -59,13 +56,24 @@ namespace Электронная_Фармация.HelpForms AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}"); ToastNotification.ShowSuccess("Авторизация успешно выполнена"); - // После входа обязательно запрашиваем Location ID для отправки заказов. - if (!HF_LocationId.PromptAndSave(this)) + // Fallback Location ID (на аптеку задаётся у грузополучателя). + if (string.IsNullOrWhiteSpace(AppConfig.LocationId)) { - ToastNotification.ShowCustom( - "Location ID не указан. Отправка заказов будет недоступна, пока его не заполните.", - System.Drawing.Color.DarkOrange, - System.Drawing.Color.White); + if (HF_LocationId.PromptAndSave(this)) + { + ConsigneeHelper.ApplyLocationIdToEmptyConsignees(AppConfig.LocationId); + } + else + { + ToastNotification.ShowCustom( + "Location ID не указан. Задайте его у грузополучателя или в настройках.", + System.Drawing.Color.DarkOrange, + System.Drawing.Color.White); + } + } + else + { + ConsigneeHelper.ApplyLocationIdToEmptyConsignees(AppConfig.LocationId); } DialogResult = DialogResult.OK; @@ -97,9 +105,10 @@ namespace Электронная_Фармация.HelpForms { UiThemeHelper.ApplyToControlTree(this); WindowChromeHelper.ApplyDialogChrome(this, panel1, label3); - txtLogin.Text = login; - txtPassword.Text = password; - checkTokenGained.Checked = doWeHaveAToken.Length > 0; + // Только последний успешный логин; пароль пользователь вводит каждый раз. + txtLogin.Text = Settings.Default.stringLogin ?? string.Empty; + txtPassword.Text = string.Empty; + checkTokenGained.Checked = !string.IsNullOrWhiteSpace(Settings.Default.stringToken); } } } diff --git a/src/ElectronicPharmacy/HelpForms/HF_Settings.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_Settings.Designer.cs index d61c6aa..3b93f40 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_Settings.Designer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_Settings.Designer.cs @@ -59,7 +59,7 @@ namespace Электронная_Фармация.HelpForms this.lblHint.Name = "lblHint"; this.lblHint.Size = new System.Drawing.Size(512, 46); this.lblHint.TabIndex = 1; - this.lblHint.Text = "Здесь можно задать торговую точку, адрес API и папку для экспорта накладных."; + this.lblHint.Text = "Location ID ниже — запасной. Для каждой аптеки задайте свой ID в «Грузополучатели». Также укажите API и папку накладных."; // // lblLocationId //