From a6601567f2a196f88820aacc7949d66681ec8091 Mon Sep 17 00:00:00 2001 From: Magomed Date: Thu, 16 Jul 2026 16:31:00 +0300 Subject: [PATCH] Improve order UX and harden API debugging. Prompt for Location ID after login, move cart delete there, refine price-list Backspace navigation, and log every API failure with stack and response body. Co-authored-by: Cursor --- src/ElectronicPharmacy/Classes/ApiClient.cs | 96 ++++++++--- src/ElectronicPharmacy/Classes/AppConfig.cs | 6 + src/ElectronicPharmacy/Classes/AppDebugLog.cs | 39 ++++- src/ElectronicPharmacy/Classes/DataSender.cs | 68 ++++++-- .../Classes/InvoiceSyncService.cs | 138 ++++++++------- .../ElectronicPharmacy.csproj | 6 + src/ElectronicPharmacy/Form1.cs | 2 + .../HelpForms/HF_LocationId.Designer.cs | 131 +++++++++++++++ .../HelpForms/HF_LocationId.cs | 62 +++++++ .../HelpForms/HF_Registration.Designer.cs | 41 +---- .../HelpForms/HF_Registration.cs | 55 +++--- .../HelpForms/HF_UploadDataToServer.cs | 3 +- .../UserControls/FOrders.cs | 13 +- .../UserControls/UCInvoices.Designer.cs | 3 +- .../UserControls/UCInvoices.cs | 159 +++++++++++++++++- .../UserControls/UCOrderItems.Designer.cs | 15 ++ .../UserControls/UCOrderItems.cs | 72 ++++++++ .../UserControls/UCPriceList.Designer.cs | 14 -- .../UserControls/UCPriceList.cs | 142 +++++++--------- .../UserControls/UCRefusals.cs | 1 + 20 files changed, 799 insertions(+), 267 deletions(-) create mode 100644 src/ElectronicPharmacy/HelpForms/HF_LocationId.Designer.cs create mode 100644 src/ElectronicPharmacy/HelpForms/HF_LocationId.cs diff --git a/src/ElectronicPharmacy/Classes/ApiClient.cs b/src/ElectronicPharmacy/Classes/ApiClient.cs index f955928..ef228c6 100644 --- a/src/ElectronicPharmacy/Classes/ApiClient.cs +++ b/src/ElectronicPharmacy/Classes/ApiClient.cs @@ -118,10 +118,18 @@ namespace Электронная_Фармация.Classes } var responseBody = await SendAuthorizedGetAsync(path, "загрузку прайса"); - var summary = JsonSerializer.Deserialize(responseBody, JsonOptions); - var count = summary?.Summary?.Count ?? 0; - AppDebugLog.Info("ApiClient", $"Прайс получен: {count} позиций"); - return summary; + try + { + var summary = JsonSerializer.Deserialize(responseBody, JsonOptions); + var count = summary?.Summary?.Count ?? 0; + AppDebugLog.Info("ApiClient", $"Прайс получен: {count} позиций"); + return summary; + } + catch (Exception ex) + { + AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ прайса", ex); + throw new HttpRequestException($"Некорректный ответ сервера при загрузке прайса: {ex.Message}", ex); + } } public async Task> GetInvoicesAsync() @@ -129,16 +137,26 @@ namespace Электронная_Фармация.Classes EnsureAuthenticated(); var responseBody = await SendAuthorizedGetAsync("/api/buyer/invoices", "загрузку накладных"); - var items = JsonSerializer.Deserialize>(responseBody, JsonOptions) ?? new List(); - AppDebugLog.Info("ApiClient", $"Накладные получены: {items.Count} шт."); - return items; + try + { + var items = JsonSerializer.Deserialize>(responseBody, JsonOptions) ?? new List(); + AppDebugLog.Info("ApiClient", $"Накладные получены: {items.Count} шт."); + return items; + } + catch (Exception ex) + { + AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ накладных", ex); + throw new HttpRequestException($"Некорректный ответ сервера при загрузке накладных: {ex.Message}", ex); + } } private void EnsureAuthenticated() { if (!IsAuthenticated) { - throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных"); + var ex = new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных"); + AppDebugLog.Error("ApiClient", "Запрос без токена авторизации", ex); + throw ex; } } @@ -169,11 +187,19 @@ namespace Электронная_Фармация.Classes { _token = null; var error = TryDeserializeError(responseBody); - throw new UnauthorizedAccessException($"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}"); + var message = $"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}"; + AppDebugLog.ApiHttpError("GET", fullUrl, (int)response.StatusCode, responseBody, operationName); + var unauthorized = new UnauthorizedAccessException(message); + AppDebugLog.Error("ApiClient", message, unauthorized); + throw unauthorized; } var httpError = TryDeserializeError(responseBody); - throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}"); + var httpMessage = $"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}"; + AppDebugLog.ApiHttpError("GET", fullUrl, (int)response.StatusCode, responseBody, operationName); + var httpEx = new HttpRequestException(httpMessage); + AppDebugLog.Error("ApiClient", httpMessage, httpEx); + throw httpEx; } } catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP")) @@ -188,7 +214,15 @@ namespace Электронная_Фармация.Classes AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex); throw WrapNetworkError(ex, operationName); } - catch (Exception ex) when (!(ex is UnauthorizedAccessException) && !(ex is HttpRequestException)) + catch (UnauthorizedAccessException) + { + throw; + } + catch (HttpRequestException) + { + throw; + } + catch (Exception ex) { stopwatch.Stop(); AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex); @@ -200,25 +234,47 @@ namespace Электронная_Фармация.Classes { if (response.IsSuccessStatusCode) { - var loginResponse = JsonSerializer.Deserialize(responseBody, JsonOptions); - if (loginResponse == null || string.IsNullOrWhiteSpace(loginResponse.Token)) + try { - throw new HttpRequestException($"Сервер {_baseUrl} вернул пустой токен авторизации"); - } + var loginResponse = JsonSerializer.Deserialize(responseBody, JsonOptions); + if (loginResponse == null || string.IsNullOrWhiteSpace(loginResponse.Token)) + { + var emptyTokenEx = new HttpRequestException($"Сервер {_baseUrl} вернул пустой токен авторизации"); + AppDebugLog.Error("ApiClient", "Пустой токен в ответе логина", emptyTokenEx); + throw emptyTokenEx; + } - _token = loginResponse.Token; - AppDebugLog.Info("ApiClient", $"Авторизация успешна. token={AppDebugLog.MaskToken(_token)}"); - return _token; + _token = loginResponse.Token; + AppDebugLog.Info("ApiClient", $"Авторизация успешна. token={AppDebugLog.MaskToken(_token)}"); + return _token; + } + catch (HttpRequestException) + { + throw; + } + catch (Exception ex) + { + AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ логина", ex); + throw new HttpRequestException($"Некорректный ответ сервера при авторизации: {ex.Message}", ex); + } } if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { var error = TryDeserializeError(responseBody); - throw new UnauthorizedAccessException($"Ошибка авторизации: {error?.Error ?? "Неверные учетные данные"}"); + var message = $"Ошибка авторизации: {error?.Error ?? "Неверные учетные данные"}"; + AppDebugLog.ApiHttpError("POST", BuildFullUrl("/auth/login"), (int)response.StatusCode, responseBody, "login"); + var unauthorized = new UnauthorizedAccessException(message); + AppDebugLog.Error("ApiClient", message, unauthorized); + throw unauthorized; } var httpError = TryDeserializeError(responseBody); - throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}"); + var httpMessage = $"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}"; + AppDebugLog.ApiHttpError("POST", BuildFullUrl("/auth/login"), (int)response.StatusCode, responseBody, "login"); + var httpEx = new HttpRequestException(httpMessage); + AppDebugLog.Error("ApiClient", httpMessage, httpEx); + throw httpEx; } private string BuildFullUrl(string path) diff --git a/src/ElectronicPharmacy/Classes/AppConfig.cs b/src/ElectronicPharmacy/Classes/AppConfig.cs index 505bc28..f606bcd 100644 --- a/src/ElectronicPharmacy/Classes/AppConfig.cs +++ b/src/ElectronicPharmacy/Classes/AppConfig.cs @@ -102,5 +102,11 @@ namespace Электронная_Фармация.Classes { get { return Settings.Default.LocationId ?? string.Empty; } } + + public static void SetLocationId(string locationId) + { + Settings.Default.LocationId = (locationId ?? string.Empty).Trim(); + Settings.Default.Save(); + } } } diff --git a/src/ElectronicPharmacy/Classes/AppDebugLog.cs b/src/ElectronicPharmacy/Classes/AppDebugLog.cs index 3974b87..a9ac056 100644 --- a/src/ElectronicPharmacy/Classes/AppDebugLog.cs +++ b/src/ElectronicPharmacy/Classes/AppDebugLog.cs @@ -13,7 +13,7 @@ namespace Электронная_Фармация.Classes public static class AppDebugLog { private static readonly object Sync = new object(); - private const int MaxBodyLength = 4000; + private const int MaxBodyLength = 8000; private const string DisableEnvVar = "ELF_API_DEBUG"; private const string DisableMarkerFile = "api-debug.off"; @@ -88,10 +88,36 @@ namespace Электронная_Фармация.Classes public static void ApiFailure(string method, string url, long elapsedMs, Exception exception) { - var message = $"{method} {url} failed after {elapsedMs} ms: {exception.Message}"; + var message = $"{method} {url} failed after {elapsedMs} ms: {exception?.Message}"; Write("FAIL", "Api", message, exception); } + /// + /// Логирует ошибку HTTP-ответа API (4xx/5xx) с телом ответа. + /// + public static void ApiHttpError( + string method, + string url, + int statusCode, + string responseBody, + string context = null) + { + var message = new StringBuilder(); + message.Append(method).Append(" ").Append(url); + message.Append(" -> HTTP ").Append(statusCode); + if (!string.IsNullOrWhiteSpace(context)) + { + message.Append(" | ").Append(context); + } + + if (!string.IsNullOrWhiteSpace(responseBody)) + { + message.Append(" | body=").Append(Truncate(SanitizeJson(responseBody))); + } + + Write("HTTP-ERR", "Api", message.ToString(), null); + } + public static string MaskToken(string token) { if (string.IsNullOrWhiteSpace(token)) @@ -148,6 +174,12 @@ namespace Электронная_Фармация.Classes } sb.Append(current.GetType().Name).Append(": ").Append(current.Message); + if (!string.IsNullOrWhiteSpace(current.StackTrace) && depth == 0) + { + sb.AppendLine(); + sb.Append(current.StackTrace); + } + current = current.InnerException; depth++; } @@ -172,7 +204,8 @@ namespace Электронная_Фармация.Classes if (exception != null) { - line.Append(" | ").Append(FormatException(exception)); + line.AppendLine(); + line.Append(FormatException(exception)); } lock (Sync) diff --git a/src/ElectronicPharmacy/Classes/DataSender.cs b/src/ElectronicPharmacy/Classes/DataSender.cs index fb11e8d..37fb405 100644 --- a/src/ElectronicPharmacy/Classes/DataSender.cs +++ b/src/ElectronicPharmacy/Classes/DataSender.cs @@ -10,6 +10,8 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; +using System.Windows.Forms; +using Электронная_Фармация.HelpForms; using Электронная_Фармация.Properties; namespace Электронная_Фармация.Classes @@ -28,20 +30,36 @@ namespace Электронная_Фармация.Classes if (string.IsNullOrWhiteSpace(_token)) { + AppDebugLog.Error("DataSender", "Отправка заказов: токен отсутствует"); ToastNotification.ShowError("Не найден токен авторизации."); return; } if (string.IsNullOrWhiteSpace(AppConfig.LocationId)) { - ToastNotification.ShowCustom( - "Не указан location_id. Заполните его в окне авторизации/настроек.", - Color.DarkOrange, - Color.White); - return; + if (!HF_LocationId.PromptAndSave(Form.ActiveForm) + || string.IsNullOrWhiteSpace(AppConfig.LocationId)) + { + AppDebugLog.Error("DataSender", "Отправка заказов: location_id не указан"); + ToastNotification.ShowCustom( + "Не указан location_id. Заполните его после авторизации.", + Color.DarkOrange, + Color.White); + return; + } } - var orders = FetchDataFromSql(null); + List orders; + try + { + orders = FetchDataFromSql(null); + } + catch (Exception ex) + { + AppDebugLog.Error("DataSender", "Ошибка подготовки заказов к отправке", ex); + ToastNotification.ShowError($"Ошибка подготовки заказов: {ex.Message}"); + return; + } if (orders.Count == 0) { ToastNotification.ShowCustom("Нет новых заказов для отправки.", Color.DarkOrange, Color.White); @@ -87,20 +105,36 @@ namespace Электронная_Фармация.Classes if (string.IsNullOrWhiteSpace(_token)) { + AppDebugLog.Error("DataSender", $"Отправка заказа {localOrderId}: токен отсутствует"); ToastNotification.ShowError("Не найден токен авторизации."); return false; } if (string.IsNullOrWhiteSpace(AppConfig.LocationId)) { - ToastNotification.ShowCustom( - "Не указан location_id. Заполните его в окне авторизации/настроек.", - Color.DarkOrange, - Color.White); - return false; + 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; + } } - var orders = FetchDataFromSql(localOrderId); + List orders; + try + { + orders = FetchDataFromSql(localOrderId); + } + catch (Exception ex) + { + AppDebugLog.Error("DataSender", $"Ошибка подготовки заказа {localOrderId}", ex); + ToastNotification.ShowError($"Ошибка подготовки заказа: {ex.Message}"); + return false; + } if (orders.Count == 0) { ToastNotification.ShowCustom("Заказ не найден или уже отправлен.", Color.DarkOrange, Color.White); @@ -218,7 +252,15 @@ WHERE Orders.OrderState = 'НОВЫЙ' if (!response.IsSuccessStatusCode) { - AppDebugLog.Error("DataSender", $"Ошибка отправки заказа {order.LocalOrderId}"); + AppDebugLog.ApiHttpError( + "POST", + fullUrl, + (int)response.StatusCode, + responseString, + $"order={order.LocalOrderId}"); + AppDebugLog.Error( + "DataSender", + $"Ошибка отправки заказа {order.LocalOrderId}: HTTP {(int)response.StatusCode} {response.ReasonPhrase}"); ToastNotification.ShowError( $"Ошибка отправки заказа {order.LocalOrderId}: {(int)response.StatusCode} {response.ReasonPhrase}\n{responseString}"); return false; diff --git a/src/ElectronicPharmacy/Classes/InvoiceSyncService.cs b/src/ElectronicPharmacy/Classes/InvoiceSyncService.cs index 402f959..d8a7107 100644 --- a/src/ElectronicPharmacy/Classes/InvoiceSyncService.cs +++ b/src/ElectronicPharmacy/Classes/InvoiceSyncService.cs @@ -1,62 +1,76 @@ -using System.Collections.Generic; -using System.Data.SQLite; -using System.Threading.Tasks; -using Электронная_Фармация.Properties; - -namespace Электронная_Фармация.Classes -{ - /// - /// Synchronizes electronic invoices and refusals from API into local SQLite. - /// - public sealed class InvoiceSyncService - { - public async Task SyncFromServerAsync() - { - var token = Settings.Default.stringToken; - if (string.IsNullOrWhiteSpace(token)) - { - throw new System.InvalidOperationException("Необходима авторизация перед загрузкой накладных."); - } - - var client = new ApiClient(); - client.SetToken(token); - var items = await client.GetInvoicesAsync(); - return SaveInvoices(items); - } - - private static int SaveInvoices(IReadOnlyList items) - { - if (items.Count == 0) - { - return 0; - } - - using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString)) - { - con.Open(); - using (var tx = con.BeginTransaction()) - { - foreach (var item in items) - { - using (var cmd = new SQLiteCommand(@" -INSERT OR REPLACE INTO Invoice (InvoiceNumber, invoiceDate, SupplierName, ConsigneesName, InvoiceSum, RefuseSum) -VALUES (@number, @date, @supplier, @consignee, @sum, @refuseSum)", con, tx)) - { - cmd.Parameters.AddWithValue("@number", item.InvoiceNumber ?? string.Empty); - cmd.Parameters.AddWithValue("@date", item.InvoiceDate ?? string.Empty); - cmd.Parameters.AddWithValue("@supplier", item.SupplierName ?? string.Empty); - cmd.Parameters.AddWithValue("@consignee", item.ConsigneeName ?? string.Empty); - cmd.Parameters.AddWithValue("@sum", item.InvoiceSum ?? string.Empty); - cmd.Parameters.AddWithValue("@refuseSum", item.RefuseSum ?? string.Empty); - cmd.ExecuteNonQuery(); - } - } - - tx.Commit(); - } - } - - return items.Count; - } - } -} +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.Threading.Tasks; +using Электронная_Фармация.Properties; + +namespace Электронная_Фармация.Classes +{ + /// + /// Synchronizes electronic invoices and refusals from API into local SQLite. + /// + public sealed class InvoiceSyncService + { + public async Task SyncFromServerAsync() + { + var token = Settings.Default.stringToken; + if (string.IsNullOrWhiteSpace(token)) + { + var ex = new InvalidOperationException("Необходима авторизация перед загрузкой накладных."); + AppDebugLog.Error("InvoiceSync", "Синхронизация накладных без токена", ex); + throw ex; + } + + try + { + var client = new ApiClient(); + client.SetToken(token); + AppDebugLog.Info("InvoiceSync", $"Старт синхронизации накладных. Сервер={client.BaseUrl}"); + var items = await client.GetInvoicesAsync(); + var saved = SaveInvoices(items); + AppDebugLog.Info("InvoiceSync", $"Накладные сохранены локально: {saved} шт."); + return saved; + } + catch (Exception ex) + { + AppDebugLog.Error("InvoiceSync", "Ошибка синхронизации накладных с API", ex); + throw; + } + } + + private static int SaveInvoices(IReadOnlyList items) + { + if (items.Count == 0) + { + return 0; + } + + using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + con.Open(); + using (var tx = con.BeginTransaction()) + { + foreach (var item in items) + { + using (var cmd = new SQLiteCommand(@" +INSERT OR REPLACE INTO Invoice (InvoiceNumber, invoiceDate, SupplierName, ConsigneesName, InvoiceSum, RefuseSum) +VALUES (@number, @date, @supplier, @consignee, @sum, @refuseSum)", con, tx)) + { + cmd.Parameters.AddWithValue("@number", item.InvoiceNumber ?? string.Empty); + cmd.Parameters.AddWithValue("@date", item.InvoiceDate ?? string.Empty); + cmd.Parameters.AddWithValue("@supplier", item.SupplierName ?? string.Empty); + cmd.Parameters.AddWithValue("@consignee", item.ConsigneeName ?? string.Empty); + cmd.Parameters.AddWithValue("@sum", item.InvoiceSum ?? string.Empty); + cmd.Parameters.AddWithValue("@refuseSum", item.RefuseSum ?? string.Empty); + cmd.ExecuteNonQuery(); + } + } + + tx.Commit(); + } + } + + return items.Count; + } + } +} diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj index 68e384c..aace520 100644 --- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj +++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj @@ -175,6 +175,12 @@ HF_DownloadDataFromServer.cs + + Form + + + HF_LocationId.cs + Form diff --git a/src/ElectronicPharmacy/Form1.cs b/src/ElectronicPharmacy/Form1.cs index b3b25b9..5128c97 100644 --- a/src/ElectronicPharmacy/Form1.cs +++ b/src/ElectronicPharmacy/Form1.cs @@ -500,6 +500,7 @@ namespace Электронная_Фармация } catch (Exception ex) { + AppDebugLog.Error("Form1", "Не удалось отправить данные (меню Обмен)", ex); ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}"); //MessageBox.Show(ex.ToString(), "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error); //MessageBox.Show($"Не удалось отправить данные на сервер. Текст ошибки:\n{ex.ToString()}", "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error); @@ -604,6 +605,7 @@ namespace Электронная_Фармация } catch (Exception ex) { + AppDebugLog.Error("Form1", "Не удалось отправить данные (кнопка ОТПРАВИТЬ)", ex); ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}"); } finally diff --git a/src/ElectronicPharmacy/HelpForms/HF_LocationId.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_LocationId.Designer.cs new file mode 100644 index 0000000..5215bbc --- /dev/null +++ b/src/ElectronicPharmacy/HelpForms/HF_LocationId.Designer.cs @@ -0,0 +1,131 @@ +namespace Электронная_Фармация.HelpForms +{ + partial class HF_LocationId + { + private System.ComponentModel.IContainer components = null; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + private void InitializeComponent() + { + this.panelHeader = new System.Windows.Forms.Panel(); + this.lblTitle = new System.Windows.Forms.Label(); + this.lblHint = new System.Windows.Forms.Label(); + this.lblLocationId = new System.Windows.Forms.Label(); + this.txtLocationId = new Elfisa.UI.Controls.ModernTextBox(); + this.btnCancel = new Elfisa.UI.Controls.ModernButton(); + this.btnSave = new Elfisa.UI.Controls.ModernButton(); + this.panelHeader.SuspendLayout(); + this.SuspendLayout(); + // + // panelHeader + // + this.panelHeader.Controls.Add(this.lblTitle); + this.panelHeader.Dock = System.Windows.Forms.DockStyle.Top; + this.panelHeader.Location = new System.Drawing.Point(0, 0); + this.panelHeader.Name = "panelHeader"; + this.panelHeader.Size = new System.Drawing.Size(460, 56); + this.panelHeader.TabIndex = 0; + // + // lblTitle + // + this.lblTitle.Font = new System.Drawing.Font("Segoe UI Semibold", 13.8F, System.Drawing.FontStyle.Bold); + this.lblTitle.ForeColor = System.Drawing.Color.White; + this.lblTitle.Location = new System.Drawing.Point(12, 10); + this.lblTitle.Name = "lblTitle"; + this.lblTitle.Size = new System.Drawing.Size(430, 36); + this.lblTitle.TabIndex = 0; + this.lblTitle.Text = "Location ID"; + this.lblTitle.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // lblHint + // + this.lblHint.Font = new System.Drawing.Font("Segoe UI", 10F); + this.lblHint.Location = new System.Drawing.Point(20, 70); + this.lblHint.Name = "lblHint"; + this.lblHint.Size = new System.Drawing.Size(420, 48); + this.lblHint.TabIndex = 1; + this.lblHint.Text = "Укажите UUID торговой точки. Он будет использоваться при отправке заказов на сервер."; + // + // lblLocationId + // + this.lblLocationId.AutoSize = true; + this.lblLocationId.Font = new System.Drawing.Font("Segoe UI", 12F); + this.lblLocationId.Location = new System.Drawing.Point(20, 128); + this.lblLocationId.Name = "lblLocationId"; + this.lblLocationId.Size = new System.Drawing.Size(108, 28); + this.lblLocationId.TabIndex = 2; + this.lblLocationId.Text = "Location ID"; + // + // txtLocationId + // + this.txtLocationId.Font = new System.Drawing.Font("Segoe UI", 12F); + this.txtLocationId.Location = new System.Drawing.Point(24, 160); + this.txtLocationId.Name = "txtLocationId"; + this.txtLocationId.Size = new System.Drawing.Size(412, 34); + this.txtLocationId.TabIndex = 3; + // + // btnCancel + // + this.btnCancel.Location = new System.Drawing.Point(24, 220); + this.btnCancel.Name = "btnCancel"; + this.btnCancel.Size = new System.Drawing.Size(125, 40); + this.btnCancel.TabIndex = 4; + this.btnCancel.Text = "Отмена"; + this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click); + // + // btnSave + // + this.btnSave.Location = new System.Drawing.Point(221, 220); + this.btnSave.Name = "btnSave"; + this.btnSave.Size = new System.Drawing.Size(215, 40); + this.btnSave.TabIndex = 5; + this.btnSave.Text = "Сохранить"; + this.btnSave.Click += new System.EventHandler(this.btnSave_Click); + // + // HF_LocationId + // + this.AcceptButton = this.btnSave; + this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.btnCancel; + this.ClientSize = new System.Drawing.Size(460, 280); + this.Controls.Add(this.btnSave); + this.Controls.Add(this.btnCancel); + this.Controls.Add(this.txtLocationId); + this.Controls.Add(this.lblLocationId); + this.Controls.Add(this.lblHint); + this.Controls.Add(this.panelHeader); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "HF_LocationId"; + this.ShowIcon = false; + this.ShowInTaskbar = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Load += new System.EventHandler(this.HF_LocationId_Load); + this.panelHeader.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + } + + #endregion + + private System.Windows.Forms.Panel panelHeader; + private System.Windows.Forms.Label lblTitle; + private System.Windows.Forms.Label lblHint; + private System.Windows.Forms.Label lblLocationId; + private Elfisa.UI.Controls.ModernTextBox txtLocationId; + private Elfisa.UI.Controls.ModernButton btnCancel; + private Elfisa.UI.Controls.ModernButton btnSave; + } +} diff --git a/src/ElectronicPharmacy/HelpForms/HF_LocationId.cs b/src/ElectronicPharmacy/HelpForms/HF_LocationId.cs new file mode 100644 index 0000000..1ba4401 --- /dev/null +++ b/src/ElectronicPharmacy/HelpForms/HF_LocationId.cs @@ -0,0 +1,62 @@ +using System; +using System.Drawing; +using System.Windows.Forms; +using Elfisa.UI.Helpers; +using Elfisa.UI.Theming; +using Электронная_Фармация.Classes; +using Электронная_Фармация.Properties; + +namespace Электронная_Фармация.HelpForms +{ + public partial class HF_LocationId : Form + { + public HF_LocationId() + { + InitializeComponent(); + Shown += (_, __) => WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors); + } + + /// + /// Показывает диалог Location ID. Возвращает true, если значение сохранено. + /// + public static bool PromptAndSave(IWin32Window owner) + { + using (var form = new HF_LocationId()) + { + UiThemeHelper.ApplyToControlTree(form); + return form.ShowDialog(owner) == DialogResult.OK; + } + } + + private void HF_LocationId_Load(object sender, EventArgs e) + { + UiThemeHelper.ApplyToControlTree(this); + WindowChromeHelper.ApplyDialogChrome(this, panelHeader, lblTitle); + txtLocationId.Text = Settings.Default.LocationId ?? string.Empty; + txtLocationId.SelectAll(); + txtLocationId.Focus(); + } + + private void btnSave_Click(object sender, EventArgs e) + { + var locationId = (txtLocationId.Text ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(locationId)) + { + UiDialogs.ShowError("Укажите Location ID торговой точки.", "Location ID", this); + txtLocationId.Focus(); + return; + } + + AppConfig.SetLocationId(locationId); + AppDebugLog.Info("Auth", $"LocationId сохранён: {locationId}"); + DialogResult = DialogResult.OK; + Close(); + } + + private void btnCancel_Click(object sender, EventArgs e) + { + DialogResult = DialogResult.Cancel; + Close(); + } + } +} diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs index dedb66b..b9a93d7 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs @@ -38,8 +38,6 @@ this.checkTokenGained = new System.Windows.Forms.CheckBox(); this.panel1 = new System.Windows.Forms.Panel(); this.label3 = new System.Windows.Forms.Label(); - this.label4 = new System.Windows.Forms.Label(); - this.txtLocationId = new Elfisa.UI.Controls.ModernTextBox(); this.label5 = new System.Windows.Forms.Label(); this.txtApiUrl = new Elfisa.UI.Controls.ModernTextBox(); this.panel1.SuspendLayout(); @@ -83,16 +81,16 @@ // // btnConfirmRegistration // - this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 401); + this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 300); this.btnConfirmRegistration.Name = "btnConfirmRegistration"; this.btnConfirmRegistration.Size = new System.Drawing.Size(215, 43); this.btnConfirmRegistration.TabIndex = 5; - this.btnConfirmRegistration.Text = "Зарегистрировать"; + this.btnConfirmRegistration.Text = "Войти"; this.btnConfirmRegistration.Click += new System.EventHandler(this.btnConfirmRegistration_Click); // // btnCloseThis // - this.btnCloseThis.Location = new System.Drawing.Point(28, 401); + this.btnCloseThis.Location = new System.Drawing.Point(28, 300); this.btnCloseThis.Name = "btnCloseThis"; this.btnCloseThis.Size = new System.Drawing.Size(125, 43); this.btnCloseThis.TabIndex = 6; @@ -104,7 +102,7 @@ this.checkTokenGained.AutoCheck = false; this.checkTokenGained.AutoSize = true; this.checkTokenGained.Font = new System.Drawing.Font("Segoe UI", 12F); - this.checkTokenGained.Location = new System.Drawing.Point(28, 342); + this.checkTokenGained.Location = new System.Drawing.Point(28, 260); this.checkTokenGained.Name = "checkTokenGained"; this.checkTokenGained.Size = new System.Drawing.Size(171, 32); this.checkTokenGained.TabIndex = 7; @@ -135,29 +133,11 @@ this.label3.Text = "АВТОРИЗАЦИЯ"; this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleRight; // - // label4 - // - this.label4.AutoSize = true; - this.label4.Font = new System.Drawing.Font("Segoe UI", 12F); - this.label4.Location = new System.Drawing.Point(23, 225); - this.label4.Name = "label4"; - this.label4.Size = new System.Drawing.Size(108, 28); - this.label4.TabIndex = 9; - this.label4.Text = "Location ID"; - // - // txtLocationId - // - this.txtLocationId.Font = new System.Drawing.Font("Segoe UI", 12F); - this.txtLocationId.Location = new System.Drawing.Point(28, 256); - this.txtLocationId.Name = "txtLocationId"; - this.txtLocationId.Size = new System.Drawing.Size(375, 34); - this.txtLocationId.TabIndex = 10; - // // label5 // this.label5.AutoSize = true; this.label5.Font = new System.Drawing.Font("Segoe UI", 12F); - this.label5.Location = new System.Drawing.Point(23, 296); + this.label5.Location = new System.Drawing.Point(23, 225); this.label5.Name = "label5"; this.label5.Size = new System.Drawing.Size(103, 28); this.label5.TabIndex = 11; @@ -166,7 +146,7 @@ // txtApiUrl // this.txtApiUrl.Font = new System.Drawing.Font("Segoe UI", 12F); - this.txtApiUrl.Location = new System.Drawing.Point(132, 296); + this.txtApiUrl.Location = new System.Drawing.Point(132, 225); this.txtApiUrl.Name = "txtApiUrl"; this.txtApiUrl.Size = new System.Drawing.Size(271, 34); this.txtApiUrl.TabIndex = 12; @@ -175,11 +155,9 @@ // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(436, 456); + this.ClientSize = new System.Drawing.Size(436, 360); this.Controls.Add(this.txtApiUrl); this.Controls.Add(this.label5); - this.Controls.Add(this.txtLocationId); - this.Controls.Add(this.label4); this.Controls.Add(this.panel1); this.Controls.Add(this.checkTokenGained); this.Controls.Add(this.btnCloseThis); @@ -199,7 +177,6 @@ this.panel1.ResumeLayout(false); this.ResumeLayout(false); this.PerformLayout(); - } #endregion @@ -213,9 +190,7 @@ private System.Windows.Forms.CheckBox checkTokenGained; private System.Windows.Forms.Panel panel1; private System.Windows.Forms.Label label3; - private System.Windows.Forms.Label label4; - private Elfisa.UI.Controls.ModernTextBox txtLocationId; private System.Windows.Forms.Label label5; private Elfisa.UI.Controls.ModernTextBox txtApiUrl; } -} \ No newline at end of file +} diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs index 924ba2f..8069c1f 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs @@ -1,19 +1,10 @@ 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 Электронная_Фармация.Properties; -using System.Text.Json; -using System.Text.Json.Serialization; -using Электронная_Фармация.Classes; using System.Net.Http; +using System.Windows.Forms; using Elfisa.UI.Helpers; using Elfisa.UI.Theming; +using Электронная_Фармация.Classes; +using Электронная_Фармация.Properties; namespace Электронная_Фармация.HelpForms { @@ -25,21 +16,20 @@ 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(); - + 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) { - this.Close(); + Close(); } private async void btnConfirmRegistration_Click(object sender, EventArgs e) { btnConfirmRegistration.Enabled = false; btnCloseThis.Enabled = false; - this.UseWaitCursor = true; + UseWaitCursor = true; string strLogin = txtLogin.Text; string strPassword = txtPassword.Text; @@ -49,7 +39,6 @@ namespace Электронная_Фармация.HelpForms // на старый сохранённый/дефолтный адрес, а не на введённый пользователем. string apiUrl = txtApiUrl.Text.Trim(); Settings.Default.ApiBaseUrl = apiUrl; - Settings.Default.LocationId = txtLocationId.Text.Trim(); Settings.Default.Save(); LoginRequest loginRequest = new LoginRequest(); @@ -66,18 +55,24 @@ namespace Электронная_Фармация.HelpForms Settings.Default.stringLogin = strLogin; Settings.Default.stringPassword = strPassword; Settings.Default.stringToken = token; - Settings.Default.LocationId = txtLocationId.Text.Trim(); Settings.Default.ApiBaseUrl = client.BaseUrl; Settings.Default.Save(); AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}"); ToastNotification.ShowSuccess("Авторизация успешно выполнена"); - //MessageBox.Show("Данные были сохранены", "Регистрация программы успешно выполнена", MessageBoxButtons.OK, MessageBoxIcon.Information); - // Токен автоматически сохраняется в клиенте - // и будет использоваться для всех последующих запросов + // После входа обязательно запрашиваем Location ID для отправки заказов. + if (!HF_LocationId.PromptAndSave(this)) + { + ToastNotification.ShowCustom( + "Location ID не указан. Отправка заказов будет недоступна, пока его не заполните.", + System.Drawing.Color.DarkOrange, + System.Drawing.Color.White); + } + DialogResult = DialogResult.OK; Close(); + return; } catch (UnauthorizedAccessException ex) { @@ -94,10 +89,10 @@ namespace Электронная_Фармация.HelpForms { UiDialogs.ShowError("Есть незаполненные данные. Регистрация программы не пройдена", "Ошибка ввода", this); } + btnConfirmRegistration.Enabled = true; btnCloseThis.Enabled = true; - this.UseWaitCursor = false; - + UseWaitCursor = false; } private void HF_Registration_Load(object sender, EventArgs e) @@ -106,19 +101,11 @@ namespace Электронная_Фармация.HelpForms WindowChromeHelper.ApplyDialogChrome(this, panel1, label3); txtLogin.Text = login; txtPassword.Text = password; - txtLocationId.Text = Settings.Default.LocationId ?? string.Empty; txtApiUrl.Text = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl) ? AppConfig.DefaultApiBaseUrl : Settings.Default.ApiBaseUrl; - if (doWeHaveAToken.Length > 0) - { - checkTokenGained.Checked = true; - } - else - { - checkTokenGained.Checked = false; - } + checkTokenGained.Checked = doWeHaveAToken.Length > 0; } } } diff --git a/src/ElectronicPharmacy/HelpForms/HF_UploadDataToServer.cs b/src/ElectronicPharmacy/HelpForms/HF_UploadDataToServer.cs index 80d84b5..7d2ec89 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_UploadDataToServer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_UploadDataToServer.cs @@ -39,9 +39,8 @@ namespace Электронная_Фармация.HelpForms } catch(Exception ex) { + AppDebugLog.Error("Upload", "Не удалось отправить данные на сервер", ex); ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}"); - //MessageBox.Show(ex.ToString(), "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error); - //MessageBox.Show($"Не удалось отправить данные на сервер. Текст ошибки:\n{ex.ToString()}", "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } diff --git a/src/ElectronicPharmacy/UserControls/FOrders.cs b/src/ElectronicPharmacy/UserControls/FOrders.cs index de59de3..6baa273 100644 --- a/src/ElectronicPharmacy/UserControls/FOrders.cs +++ b/src/ElectronicPharmacy/UserControls/FOrders.cs @@ -355,8 +355,17 @@ namespace Электронная_Фармация.UserControls } var orderId = DGVOrders.SelectedRows[0].Cells["ИД заказа"].Value.ToString(); - var sender = new DataSender(); - await sender.SendSingleOrderAsync(orderId); + try + { + var sender = new DataSender(); + await sender.SendSingleOrderAsync(orderId); + } + catch (Exception ex) + { + AppDebugLog.Error("Orders", $"Не удалось отправить заказ {orderId}", ex); + ToastNotification.ShowError($"Не удалось отправить заказ: {ex.Message}"); + } + ApplyOrderFilters(); } diff --git a/src/ElectronicPharmacy/UserControls/UCInvoices.Designer.cs b/src/ElectronicPharmacy/UserControls/UCInvoices.Designer.cs index 8d008db..f8e1975 100644 --- a/src/ElectronicPharmacy/UserControls/UCInvoices.Designer.cs +++ b/src/ElectronicPharmacy/UserControls/UCInvoices.Designer.cs @@ -237,8 +237,7 @@ // // UCInvoices // - this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; this.Controls.Add(this.btnClearConsignees); this.Controls.Add(this.btnClearSuppliers); this.Controls.Add(this.PContentInvoice); diff --git a/src/ElectronicPharmacy/UserControls/UCInvoices.cs b/src/ElectronicPharmacy/UserControls/UCInvoices.cs index 8d2a801..05fb61a 100644 --- a/src/ElectronicPharmacy/UserControls/UCInvoices.cs +++ b/src/ElectronicPharmacy/UserControls/UCInvoices.cs @@ -18,6 +18,8 @@ namespace Электронная_Фармация.UserControls private string _initialSourceOrderId; private ContextMenuStrip _invoiceMenu; private ToolStripMenuItem _exportInvoiceMenuItem; + private Panel panelToolbar; + private bool _toolbarReady; public UCInvoices() : this(null) @@ -210,18 +212,170 @@ where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)"; UiThemeHelper.ApplyToControlTree(this); Font = new Font("Segoe UI", 9.75f, FontStyle.Regular); InitializeInvoiceContextMenu(); + EnsureInvoiceToolbar(); button1.Visible = true; button1.Text = "Обновить с сервера"; - button1.Width = 180; ModernButtonStyles.ApplyAuto(button1); - button1.Click += async (_, __) => await SyncFromServerAsync(); + button1.Click -= Button1_SyncClick; + button1.Click += Button1_SyncClick; + + ModernButtonStyles.ApplyClear(btnClearSuppliers); + ModernButtonStyles.ApplyClear(btnClearConsignees); + btnClearSuppliers.Text = "×"; + btnClearConsignees.Text = "×"; + + ApplyToolbarLayout(); loadConsignees(); loadSuppliers(); getDatesFromCalendar(); } + private async void Button1_SyncClick(object sender, EventArgs e) + { + await SyncFromServerAsync(); + } + + private void EnsureInvoiceToolbar() + { + if (_toolbarReady) + { + return; + } + + panelToolbar = new Panel + { + Name = "panelToolbar", + Dock = DockStyle.Top, + Height = 96, + Padding = new Padding(12, 10, 12, 14), + Tag = "toolbar" + }; + + var toolbarControls = new Control[] + { + button1, + dtpDateFrom, + label1, + dtpDateTo, + txtInvoiceNumber, + comboSuppliers, + btnClearSuppliers, + comboConsignees, + btnClearConsignees + }; + + foreach (var control in toolbarControls) + { + Controls.Remove(control); + panelToolbar.Controls.Add(control); + } + + Controls.Add(panelToolbar); + panelToolbar.BringToFront(); + + PContentInvoice.Dock = DockStyle.Fill; + PContentInvoice.BringToFront(); + panelToolbar.BringToFront(); + + panelToolbar.Resize += (_, __) => ApplyToolbarLayout(); + Resize += (_, __) => ApplyToolbarLayout(); + _toolbarReady = true; + } + + private void ApplyToolbarLayout() + { + if (panelToolbar == null) + { + return; + } + + const int pad = 12; + const int gap = 8; + const int clearGap = 4; + const int controlHeight = 32; + const int buttonHeight = 34; + const int rowGap = 10; + const int refreshWidth = 180; + const int dateWidth = 112; + const int clearSize = 32; + const int invoicePreferred = 200; + const int invoiceMin = 140; + const int supplierPreferred = 170; + const int consigneePreferred = 190; + const int supplierMin = 130; + const int consigneeMin = 150; + + var panelWidth = Math.Max(panelToolbar.ClientSize.Width, 360); + int y = 10; + int x = pad; + + button1.SetBounds(x, y, refreshWidth, buttonHeight); + x = button1.Right + gap; + + dtpDateFrom.SetBounds(x, y + 1, dateWidth, controlHeight); + x = dtpDateFrom.Right + 6; + label1.AutoSize = true; + label1.Location = new Point(x, y + 5); + x = label1.Right + 6; + dtpDateTo.SetBounds(x, y + 1, dateWidth, controlHeight); + x = dtpDateTo.Right + gap; + + int invoiceWidth = Math.Max(invoiceMin, Math.Min(invoicePreferred, panelWidth - pad - x)); + if (invoiceWidth < invoiceMin || x + invoiceMin > panelWidth - pad) + { + y += buttonHeight + rowGap; + x = pad; + invoiceWidth = Math.Max(invoiceMin, Math.Min(invoicePreferred, panelWidth - pad * 2)); + } + + txtInvoiceNumber.SetBounds(x, y + 1, invoiceWidth, controlHeight); + + y += buttonHeight + rowGap; + x = pad; + + int filtersAvailable = panelWidth - pad * 2 - clearSize * 2 - clearGap * 2 - gap; + int supplierWidth; + int consigneeWidth; + if (filtersAvailable >= supplierPreferred + consigneePreferred) + { + supplierWidth = supplierPreferred; + consigneeWidth = consigneePreferred; + } + else + { + float scale = filtersAvailable / (float)(supplierPreferred + consigneePreferred); + supplierWidth = Math.Max(supplierMin, (int)(supplierPreferred * scale)); + consigneeWidth = Math.Max(consigneeMin, filtersAvailable - supplierWidth); + } + + if (pad + supplierWidth + clearGap + clearSize + gap + consigneeWidth + clearGap + clearSize > panelWidth) + { + // Wrap consignee to next row on very narrow widths. + supplierWidth = Math.Max(supplierMin, panelWidth - pad * 2 - clearGap - clearSize); + comboSuppliers.SetBounds(x, y + 1, supplierWidth, controlHeight); + btnClearSuppliers.SetBounds(comboSuppliers.Right + clearGap, y, clearSize, controlHeight); + + y += buttonHeight + rowGap; + x = pad; + consigneeWidth = Math.Max(consigneeMin, panelWidth - pad * 2 - clearGap - clearSize); + comboConsignees.SetBounds(x, y + 1, consigneeWidth, controlHeight); + btnClearConsignees.SetBounds(comboConsignees.Right + clearGap, y, clearSize, controlHeight); + } + else + { + comboSuppliers.SetBounds(x, y + 1, supplierWidth, controlHeight); + btnClearSuppliers.SetBounds(comboSuppliers.Right + clearGap, y, clearSize, controlHeight); + x = btnClearSuppliers.Right + gap; + comboConsignees.SetBounds(x, y + 1, consigneeWidth, controlHeight); + btnClearConsignees.SetBounds(comboConsignees.Right + clearGap, y, clearSize, controlHeight); + } + + panelToolbar.Height = y + buttonHeight + pad + 4; + panelToolbar.Padding = new Padding(pad, 10, pad, 14); + } + private async Task SyncFromServerAsync() { button1.Enabled = false; @@ -235,6 +389,7 @@ where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)"; } catch (Exception ex) { + AppDebugLog.Error("Invoices", "Не удалось загрузить накладные с API", ex); ToastNotification.ShowError($"Не удалось загрузить накладные: {ex.Message}"); } finally diff --git a/src/ElectronicPharmacy/UserControls/UCOrderItems.Designer.cs b/src/ElectronicPharmacy/UserControls/UCOrderItems.Designer.cs index 80cfc67..ca98086 100644 --- a/src/ElectronicPharmacy/UserControls/UCOrderItems.Designer.cs +++ b/src/ElectronicPharmacy/UserControls/UCOrderItems.Designer.cs @@ -32,6 +32,7 @@ this.dgvOrder = new Elfisa.UI.Controls.ModernDataGridView(); this.lblSumOfOrder = new System.Windows.Forms.Label(); this.btnEraseOrder = new Elfisa.UI.Controls.ModernButton(); + this.btnDeleteGood = new Elfisa.UI.Controls.ModernButton(); this.btnSendOrder = new Elfisa.UI.Controls.ModernButton(); this.button1 = new Elfisa.UI.Controls.ModernButton(); this.lblMinimalSumOfOrder = new System.Windows.Forms.Label(); @@ -93,6 +94,18 @@ this.btnEraseOrder.Text = "Очистить корзину"; this.btnEraseOrder.Click += new System.EventHandler(this.btnEraseOrder_Click); // + // btnDeleteGood + // + this.btnDeleteGood.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnDeleteGood.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold); + this.btnDeleteGood.Location = new System.Drawing.Point(538, 36); + this.btnDeleteGood.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.btnDeleteGood.Name = "btnDeleteGood"; + this.btnDeleteGood.Size = new System.Drawing.Size(260, 38); + this.btnDeleteGood.TabIndex = 17; + this.btnDeleteGood.Text = "Удалить товар"; + this.btnDeleteGood.Click += new System.EventHandler(this.btnDeleteGood_Click); + // // btnSendOrder // this.btnSendOrder.FlatStyle = System.Windows.Forms.FlatStyle.Flat; @@ -184,6 +197,7 @@ this.Controls.Add(this.btnSaveAndSendOrder); this.Controls.Add(this.lblMinimalSumOfOrder); this.Controls.Add(this.lblSumOfOrder); + this.Controls.Add(this.btnDeleteGood); this.Controls.Add(this.btnEraseOrder); this.Controls.Add(this.btnSendOrder); this.Controls.Add(this.button1); @@ -205,6 +219,7 @@ private Elfisa.UI.Controls.ModernDataGridView dgvOrder; private System.Windows.Forms.Label lblSumOfOrder; private Elfisa.UI.Controls.ModernButton btnEraseOrder; + private Elfisa.UI.Controls.ModernButton btnDeleteGood; private Elfisa.UI.Controls.ModernButton btnSendOrder; private Elfisa.UI.Controls.ModernButton button1; private System.Windows.Forms.Label lblMinimalSumOfOrder; diff --git a/src/ElectronicPharmacy/UserControls/UCOrderItems.cs b/src/ElectronicPharmacy/UserControls/UCOrderItems.cs index f59a5f9..4ec0789 100644 --- a/src/ElectronicPharmacy/UserControls/UCOrderItems.cs +++ b/src/ElectronicPharmacy/UserControls/UCOrderItems.cs @@ -248,6 +248,78 @@ and ConsigneesName = '{consigneeName}' } } + private void btnDeleteGood_Click(object sender, EventArgs e) + { + if (dgvOrder.SelectedRows.Count == 0 || dgvOrder.CurrentRow == null) + { + ToastNotification.ShowCustom("Выберите наименование в корзине для удаления.", Color.DarkOrange, Color.White); + return; + } + + var row = dgvOrder.SelectedRows[0]; + string idPriceListItem = null; + + if (dgvOrder.Columns.Contains("id_PriceList_Item")) + { + idPriceListItem = row.Cells["id_PriceList_Item"]?.Value?.ToString(); + } + else if (row.Cells.Count > 11) + { + idPriceListItem = row.Cells[11]?.Value?.ToString(); + } + + if (string.IsNullOrWhiteSpace(idPriceListItem)) + { + ToastNotification.ShowCustom("Не удалось определить позицию для удаления.", Color.DarkOrange, Color.White); + return; + } + + var drugName = dgvOrder.Columns.Contains("Наименование") + ? row.Cells["Наименование"]?.Value?.ToString() + : row.Cells.Count > 3 + ? row.Cells[3]?.Value?.ToString() + : null; + + DeleteCartItem(idPriceListItem, drugName); + } + + private void DeleteCartItem(string idPriceListItem, string drugName) + { + var safeId = idPriceListItem.Replace("'", "''"); + string qClearPriceList = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where id_PriceList_Item = '{safeId}'"; + string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{safeId}'"; + + using (var sqlCon = new SQLiteConnection(connectionStringToLocalDB)) + { + try + { + sqlCon.Open(); + using (var cmdClear = new SQLiteCommand(qClearPriceList, sqlCon)) + using (var cmdDelete = new SQLiteCommand(qDeleteFromTempOrder, sqlCon)) + { + cmdClear.ExecuteNonQuery(); + cmdDelete.ExecuteNonQuery(); + } + + if (!string.IsNullOrWhiteSpace(drugName)) + { + ToastNotification.ShowSuccess($"Удалено: {drugName}"); + } + } + catch (Exception ex) + { + ToastNotification.ShowError($"Ошибка удаления товара: {ex.Message}"); + } + finally + { + sqlCon.Close(); + loadDataAboutOrderV2(_supplierName); + ParentForm?.loadPriceListV2(); + ParentForm?.loadTempOrderItems(); + } + } + } + void EraseOrder(string Supplier) { using(SQLiteConnection sqlConForDeleteTempOrder = new SQLiteConnection(connectionStringToLocalDB)) diff --git a/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs b/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs index cb0495e..8a56067 100644 --- a/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs +++ b/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs @@ -36,7 +36,6 @@ this.TLPPriceList = new System.Windows.Forms.TableLayoutPanel(); this.dgvPriceList = new Elfisa.UI.Controls.ModernDataGridView(); this.panel1 = new System.Windows.Forms.Panel(); - this.btnDeleteGood = new Elfisa.UI.Controls.ModernButton(); this.checkUsePercentageAsDefault = new System.Windows.Forms.CheckBox(); this.lblPriceForGood = new System.Windows.Forms.Label(); this.numericPercent = new System.Windows.Forms.NumericUpDown(); @@ -149,7 +148,6 @@ // // panel1 // - this.panel1.Controls.Add(this.btnDeleteGood); this.panel1.Controls.Add(this.checkUsePercentageAsDefault); this.panel1.Controls.Add(this.lblPriceForGood); this.panel1.Controls.Add(this.numericPercent); @@ -163,17 +161,6 @@ this.panel1.Size = new System.Drawing.Size(1039, 54); this.panel1.TabIndex = 1; // - // btnDeleteGood - // - this.btnDeleteGood.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.btnDeleteGood.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold); - this.btnDeleteGood.Location = new System.Drawing.Point(8, 62); - this.btnDeleteGood.Name = "btnDeleteGood"; - this.btnDeleteGood.Size = new System.Drawing.Size(360, 34); - this.btnDeleteGood.TabIndex = 13; - this.btnDeleteGood.Text = "Удалить товар (BACKSPACE)"; - this.btnDeleteGood.Click += new System.EventHandler(this.btnDeleteGood_Click); - // // checkUsePercentageAsDefault // this.checkUsePercentageAsDefault.AutoSize = true; @@ -514,6 +501,5 @@ private Elfisa.UI.Controls.ModernButton btnClearSupplierFilter; private System.Windows.Forms.CheckBox checkUsePercentageAsDefault; private System.Windows.Forms.Timer timerForResetSearch; - private Elfisa.UI.Controls.ModernButton btnDeleteGood; } } diff --git a/src/ElectronicPharmacy/UserControls/UCPriceList.cs b/src/ElectronicPharmacy/UserControls/UCPriceList.cs index b27840a..5da0f28 100644 --- a/src/ElectronicPharmacy/UserControls/UCPriceList.cs +++ b/src/ElectronicPharmacy/UserControls/UCPriceList.cs @@ -75,7 +75,6 @@ namespace Электронная_Фармация.UserControls UiThemeHelper.ApplyToControlTree(this); Tag = "chrome-child"; - btnDeleteGood.Height = 34; AlignSearchRow(); panel1.SizeChanged += (_, __) => LayoutMarkupRow(); @@ -870,6 +869,61 @@ namespace Электронная_Фармация.UserControls ShowBrowseView(); } + private void HandleBackspaceInPriceList(int rowIndex, string idPriceListItem) + { + if (_browseMode == PriceBrowseMode.Offers) + { + if (rowIndex >= 0 + && rowIndex < dgvPriceList.Rows.Count + && dgvPriceList.Rows[rowIndex].Cells.Count > 9) + { + var qty = dgvPriceList.Rows[rowIndex].Cells[9].Value?.ToString() ?? string.Empty; + if (!string.IsNullOrWhiteSpace(qty)) + { + if (!string.IsNullOrEmpty(idPriceListItem)) + { + deleteGood(rowIndex, idPriceListItem); + } + else + { + dgvPriceList.Rows[rowIndex].Cells[9].Value = string.Empty; + dgvPriceList.Rows[rowIndex].Cells[10].Value = string.Empty; + } + + return; + } + } + + ResetBrowseToNames(); + if (dgvBrowseNames != null && dgvBrowseNames.Visible) + { + dgvBrowseNames.Focus(); + } + + return; + } + + if (GoodsFilter.Length == 0) + { + return; + } + + GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1); + txtSearchGoodNameInPriceList.Text = GoodsFilter; + ResetBrowseToNames(); + + if (GoodsFilter.Length == 0) + { + timerForResetSearch.Enabled = false; + timerForResetSearch.Stop(); + } + else + { + timerForResetSearch.Enabled = true; + timerForResetSearch.Start(); + } + } + void loadInfoAboutDefaultMarkup() { @@ -1411,28 +1465,14 @@ and tempOrder.GoodName = PriceList.GoodName } } + if (e.KeyChar == (Char)Keys.Back) + { + HandleBackspaceInPriceList(RowIndex, idPriceListItem); + return; + } + if (_browseMode != PriceBrowseMode.Offers) { - if (e.KeyChar == (Char)Keys.Back && GoodsFilter.Length > 0) - { - GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1); - txtSearchGoodNameInPriceList.Text = GoodsFilter; - ResetBrowseToNames(); - - if (GoodsFilter.Length == 0) - { - timerForResetSearch.Enabled = false; - timerForResetSearch.Stop(); - } - else - { - timerForResetSearch.Enabled = true; - timerForResetSearch.Start(); - } - - return; - } - if ((e.KeyChar >= 'A' && e.KeyChar <= 'Z') || (e.KeyChar >= 'a' && e.KeyChar <= 'z') || (e.KeyChar >= 'А' && e.KeyChar <= 'Я') @@ -1443,7 +1483,7 @@ and tempOrder.GoodName = PriceList.GoodName GoodsFilter += e.KeyChar.ToString(); txtSearchGoodNameInPriceList.Text = GoodsFilter; ResetBrowseToNames(); - if (dgvPriceList.Rows.Count == 0) + if (dgvBrowseNames != null && dgvBrowseNames.Rows.Count == 0) { UiDialogs.ShowInfo("Подходящий товар не найден.", "Поиск", FindForm()); GoodsFilter = string.Empty; @@ -1455,36 +1495,6 @@ and tempOrder.GoodName = PriceList.GoodName return; } - if (e.KeyChar == (Char)Keys.Back && GoodsFilter.Length > 0) - { - GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1); - txtSearchGoodNameInPriceList.Text = GoodsFilter; - ResetBrowseToNames(); - - if (GoodsFilter.Length == 0) - { - timerForResetSearch.Enabled = false; - timerForResetSearch.Stop(); - } - else - { - timerForResetSearch.Enabled = true; - timerForResetSearch.Start(); - } - - return; - } - - if (timerForResetSearch.Enabled == false) - { - if (RowIndex >= 0 && !string.IsNullOrEmpty(idPriceListItem)) - { - deleteGood(RowIndex, idPriceListItem); - timerForResetSearch.Enabled = true; - timerForResetSearch.Start(); - } - } - if ((e.KeyChar >= 'A' && e.KeyChar <= 'Z') || (e.KeyChar >= 'a' && e.KeyChar <= 'z') || (e.KeyChar >= 'А' && e.KeyChar <= 'Я') || (e.KeyChar >= 'а' && e.KeyChar <= 'я') || (e.KeyChar == (Char)Keys.Subtract)) { if (e.KeyChar == (Char)Keys.Enter) @@ -1573,26 +1583,6 @@ and tempOrder.GoodName = PriceList.GoodName } } } - else if (e.KeyChar == (Char)Keys.Back) - { - if (RowIndex < 0) return; - - string countToBuy = dgvPriceList.Rows[RowIndex].Cells[9].Value.ToString(); - if (countToBuy.Length > 1) - { - string result = countToBuy.Remove(countToBuy.Length - 1); - - dgvPriceList.Rows[RowIndex].Cells[9].Value = result; - - if (!TryReadDecimal(dgvPriceList.Rows[RowIndex].Cells[5].Value, out var priceForOne) || - !TryReadInt32(result, out var count)) - { - return; - } - - dgvPriceList.Rows[RowIndex].Cells[10].Value = (count * priceForOne).ToString(); - } - } else if (e.KeyChar == (Char)Keys.Delete) { if (RowIndex < 0 || string.IsNullOrEmpty(idPriceListItem)) return; @@ -2341,14 +2331,6 @@ and SumOrderedItems = '{SumOrder}'"; timerForResetSearch.Stop(); } - private void btnDeleteGood_Click(object sender, EventArgs e) - { - int RowIndex = dgvPriceList.SelectedRows[0].Index; - string idPriceListItem = dgvPriceList.Rows[RowIndex].Cells[11].Value.ToString(); - deleteGood(RowIndex, idPriceListItem); - } - - private void btnMoreInfoAboutGood_Click(object sender, EventArgs e) { if (dgvPriceList.CurrentRow == null) diff --git a/src/ElectronicPharmacy/UserControls/UCRefusals.cs b/src/ElectronicPharmacy/UserControls/UCRefusals.cs index 21a1cfb..df42bdd 100644 --- a/src/ElectronicPharmacy/UserControls/UCRefusals.cs +++ b/src/ElectronicPharmacy/UserControls/UCRefusals.cs @@ -109,6 +109,7 @@ namespace Электронная_Фармация.UserControls } catch (Exception ex) { + AppDebugLog.Error("Refusals", "Не удалось загрузить отказы с API", ex); ToastNotification.ShowError($"Не удалось загрузить отказы: {ex.Message}"); } finally