From 5e24cc95b025a39f12b714528ba7247375c4a9c6 Mon Sep 17 00:00:00 2001 From: Magomed Date: Sat, 18 Jul 2026 13:27:42 +0300 Subject: [PATCH 1/3] Add animated progress bar during price list download. Show marquee while waiting for the API and determinate progress while saving rows so the UI no longer looks frozen. Co-authored-by: Cursor --- .../HF_DownloadDataFromServer.Designer.cs | 46 ++++++- .../HelpForms/HF_DownloadDataFromServer.cs | 118 ++++++++++++++++-- 2 files changed, 153 insertions(+), 11 deletions(-) diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs index e68f899..d47487a 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs @@ -32,17 +32,49 @@ this.dataGridView1 = new Elfisa.UI.Controls.ModernDataGridView(); this.DataSetPriceList = new System.Data.DataSet(); this.btnCloseThis = new Elfisa.UI.Controls.ModernButton(); + this.lblTitle = new System.Windows.Forms.Label(); + this.lblProgress = new System.Windows.Forms.Label(); + this.progressDownload = new System.Windows.Forms.ProgressBar(); ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).BeginInit(); this.SuspendLayout(); // + // lblTitle + // + this.lblTitle.AutoSize = true; + this.lblTitle.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold); + this.lblTitle.Location = new System.Drawing.Point(22, 14); + this.lblTitle.Name = "lblTitle"; + this.lblTitle.Size = new System.Drawing.Size(220, 28); + this.lblTitle.TabIndex = 5; + this.lblTitle.Text = "Загрузка прайс-листа"; + // + // progressDownload + // + this.progressDownload.Location = new System.Drawing.Point(25, 52); + this.progressDownload.MarqueeAnimationSpeed = 30; + this.progressDownload.Name = "progressDownload"; + this.progressDownload.Size = new System.Drawing.Size(630, 22); + this.progressDownload.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.progressDownload.TabIndex = 6; + // + // lblProgress + // + this.lblProgress.AutoEllipsis = true; + this.lblProgress.Font = new System.Drawing.Font("Segoe UI", 10F); + this.lblProgress.Location = new System.Drawing.Point(25, 82); + this.lblProgress.Name = "lblProgress"; + this.lblProgress.Size = new System.Drawing.Size(630, 24); + this.lblProgress.TabIndex = 7; + this.lblProgress.Text = "Подготовка..."; + // // rtxtDebug // this.rtxtDebug.BackColor = System.Drawing.SystemColors.Control; this.rtxtDebug.BorderStyle = System.Windows.Forms.BorderStyle.None; - this.rtxtDebug.Location = new System.Drawing.Point(25, 12); + this.rtxtDebug.Location = new System.Drawing.Point(25, 116); this.rtxtDebug.Name = "rtxtDebug"; - this.rtxtDebug.Size = new System.Drawing.Size(630, 319); + this.rtxtDebug.Size = new System.Drawing.Size(630, 215); this.rtxtDebug.TabIndex = 0; this.rtxtDebug.Text = ""; // @@ -79,6 +111,9 @@ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.SystemColors.Control; this.ClientSize = new System.Drawing.Size(684, 396); + this.Controls.Add(this.lblProgress); + this.Controls.Add(this.progressDownload); + this.Controls.Add(this.lblTitle); this.Controls.Add(this.btnCloseThis); this.Controls.Add(this.dataGridView1); this.Controls.Add(this.rtxtDebug); @@ -93,7 +128,7 @@ ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).EndInit(); this.ResumeLayout(false); - + this.PerformLayout(); } #endregion @@ -102,5 +137,8 @@ private Elfisa.UI.Controls.ModernDataGridView dataGridView1; private System.Data.DataSet DataSetPriceList; private Elfisa.UI.Controls.ModernButton btnCloseThis; + private System.Windows.Forms.Label lblTitle; + private System.Windows.Forms.Label lblProgress; + private System.Windows.Forms.ProgressBar progressDownload; } -} \ No newline at end of file +} diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs index 6f0a8e0..95b4dae 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs @@ -20,6 +20,7 @@ namespace Электронная_Фармация.HelpForms private readonly string _login = Settings.Default.stringLogin ?? string.Empty; private readonly string _password = Settings.Default.stringPassword ?? string.Empty; private readonly string _connectionStringToLocalDb = AppConfig.SqliteConnectionString; + private bool _isDownloading; private async void HF_DownloadDataFromServer_Load(object sender, EventArgs e) { @@ -28,6 +29,8 @@ namespace Электронная_Фармация.HelpForms dataGridView1.Visible = false; rtxtDebug.Visible = true; rtxtDebug.ReadOnly = true; + _isDownloading = true; + SetMarqueeProgress("Подключение к серверу..."); var client = new ApiClient(); AppendStatus($"Сервер: {client.BaseUrl}"); @@ -36,39 +39,52 @@ namespace Электронная_Фармация.HelpForms try { + SetMarqueeProgress("Авторизация..."); await EnsureAuthenticatedAsync(client); + SetMarqueeProgress("Загрузка прайса с сервера..."); AppendStatus("Загружаю сводный прайс (постранично, весь)..."); var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client); AppendStatus($"Получено {allSuppliersSummary.Summary?.Count ?? 0} позиций"); - var tablePrice = BuildPriceTable(allSuppliersSummary); - SavePriceTable(tablePrice); + SetMarqueeProgress("Подготовка таблицы..."); + var tablePrice = await Task.Run(() => BuildPriceTable(allSuppliersSummary)); + SetDeterminateProgress(0, Math.Max(tablePrice.Rows.Count, 1), "Сохранение в локальную базу..."); + var progress = new Progress(ReportSaveProgress); + await Task.Run(() => SavePriceTable(tablePrice, progress)); + + SetDeterminateProgress(100, 100, "Готово"); DialogResult = DialogResult.OK; AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу."); ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций"); } catch (UnauthorizedAccessException ex) { + SetMarqueeProgress("Ошибка авторизации"); AppendStatus($"Ошибка авторизации: {ex.Message}"); AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}"); } catch (HttpRequestException ex) { + SetMarqueeProgress("Ошибка сети"); AppendStatus($"Ошибка сети: {ex.Message}"); AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка сети: {ex.Message}"); } catch (Exception ex) { + SetMarqueeProgress("Ошибка загрузки"); AppendStatus($"Ошибка загрузки прайса: {ex.Message}"); AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}"); } - - btnCloseThis.Enabled = true; + finally + { + _isDownloading = false; + btnCloseThis.Enabled = true; + } } private async Task EnsureAuthenticatedAsync(ApiClient client) @@ -118,6 +134,10 @@ namespace Электронная_Фармация.HelpForms for (int page = 0; page < maxPages; page++) { int offset = page * pageSize; + SetMarqueeProgress(page == 0 + ? "Ожидание ответа сервера..." + : $"Загрузка страницы {page + 1}..."); + PriceSummaryResponse chunk; try { @@ -126,10 +146,12 @@ namespace Электронная_Фармация.HelpForms catch (UnauthorizedAccessException) { AppendStatus("Токен недействителен, повторная авторизация..."); + SetMarqueeProgress("Повторная авторизация..."); Settings.Default.stringToken = string.Empty; Settings.Default.Save(); client.SetToken(null); await LoginAndSaveTokenAsync(client); + SetMarqueeProgress("Повторная загрузка прайса..."); chunk = await client.GetPriceSummaryAsync(supplierId: null, regionId: regionId, limit: pageSize, offset: offset); } @@ -138,6 +160,7 @@ namespace Электронная_Фармация.HelpForms { combined.Summary.AddRange(chunk.Summary); AppendStatus($"Загружено {combined.Summary.Count} позиций..."); + SetMarqueeProgress($"Получено {combined.Summary.Count} позиций..."); } if (got < pageSize) @@ -215,7 +238,7 @@ namespace Электронная_Фармация.HelpForms return tablePrice; } - private void SavePriceTable(DataTable tablePrice) + private void SavePriceTable(DataTable tablePrice, IProgress progress = null) { const string commandToDelete = "delete from [PriceList]"; const string commandToDeleteTempOrder = "delete from [TempOrderItems]"; @@ -253,6 +276,7 @@ VALUES @Dosage )"; + int total = tablePrice.Rows.Count; using (var sqliteCon = new SQLiteConnection(_connectionStringToLocalDb)) { sqliteCon.Open(); @@ -266,6 +290,7 @@ VALUES using (var transaction = sqliteCon.BeginTransaction()) { + int saved = 0; foreach (DataRow row in tablePrice.Rows) { using (var cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction)) @@ -286,6 +311,12 @@ VALUES cmd.Parameters.AddWithValue("@Dosage", row.Table.Columns.Contains("Dosage") ? row["Dosage"] : DBNull.Value); cmd.ExecuteNonQuery(); } + + saved++; + if (progress != null && (saved % 250 == 0 || saved == total)) + { + progress.Report(new SaveProgress(saved, total)); + } } transaction.Commit(); @@ -293,6 +324,65 @@ VALUES } } + private void ReportSaveProgress(SaveProgress state) + { + if (IsDisposed) + { + return; + } + + int percent = state.Total <= 0 ? 0 : (int)(100.0 * state.Current / state.Total); + SetDeterminateProgress( + percent, + 100, + $"Сохранение в базу: {state.Current} / {state.Total} ({percent}%)"); + } + + private void SetMarqueeProgress(string text) + { + if (IsDisposed) + { + return; + } + + if (InvokeRequired) + { + BeginInvoke(new Action(SetMarqueeProgress), text); + return; + } + + progressDownload.Style = ProgressBarStyle.Marquee; + progressDownload.MarqueeAnimationSpeed = 30; + lblProgress.Text = text ?? string.Empty; + lblProgress.Refresh(); + progressDownload.Refresh(); + } + + private void SetDeterminateProgress(int value, int maximum, string text) + { + if (IsDisposed) + { + return; + } + + if (InvokeRequired) + { + BeginInvoke(new Action(SetDeterminateProgress), value, maximum, text); + return; + } + + if (progressDownload.Style != ProgressBarStyle.Continuous) + { + progressDownload.Style = ProgressBarStyle.Continuous; + } + + progressDownload.Maximum = Math.Max(maximum, 1); + progressDownload.Value = Math.Max(0, Math.Min(value, progressDownload.Maximum)); + lblProgress.Text = text ?? string.Empty; + lblProgress.Refresh(); + progressDownload.Refresh(); + } + private void AppendStatus(string message) { if (InvokeRequired) @@ -310,13 +400,27 @@ VALUES private void HF_DownloadDataFromServer_FormClosing(object sender, FormClosingEventArgs e) { - Dispose(); - Close(); + if (_isDownloading) + { + e.Cancel = true; + } } private void btnCloseThis_Click(object sender, EventArgs e) { Close(); } + + private sealed class SaveProgress + { + public SaveProgress(int current, int total) + { + Current = current; + Total = total; + } + + public int Current { get; } + public int Total { get; } + } } } From 9ee5561f3d26247f2c57101cf6640fce20397516 Mon Sep 17 00:00:00 2001 From: Magomed Date: Sat, 18 Jul 2026 13:34:48 +0300 Subject: [PATCH 2/3] Add invoice export folder setting and CSV export from context menu. Users can choose the export directory in auth settings; right-click on an invoice saves a UTF-8 CSV into that folder. Co-authored-by: Cursor --- src/ElectronicPharmacy/App.config | 3 + src/ElectronicPharmacy/Classes/AppConfig.cs | 11 + .../Classes/InvoiceExportService.cs | 194 ++++++++++++++++++ .../ElectronicPharmacy.csproj | 1 + .../HelpForms/HF_Registration.Designer.cs | 81 ++++++-- .../HelpForms/HF_Registration.cs | 24 +++ .../Properties/Settings.Designer.cs | 12 ++ .../Properties/Settings.settings | 3 + .../UserControls/UCInvoices.cs | 28 ++- 9 files changed, 329 insertions(+), 28 deletions(-) create mode 100644 src/ElectronicPharmacy/Classes/InvoiceExportService.cs diff --git a/src/ElectronicPharmacy/App.config b/src/ElectronicPharmacy/App.config index 6eae6c4..761ce75 100644 --- a/src/ElectronicPharmacy/App.config +++ b/src/ElectronicPharmacy/App.config @@ -36,6 +36,9 @@ + + + \ No newline at end of file diff --git a/src/ElectronicPharmacy/Classes/AppConfig.cs b/src/ElectronicPharmacy/Classes/AppConfig.cs index f606bcd..459c3dc 100644 --- a/src/ElectronicPharmacy/Classes/AppConfig.cs +++ b/src/ElectronicPharmacy/Classes/AppConfig.cs @@ -108,5 +108,16 @@ namespace Электронная_Фармация.Classes Settings.Default.LocationId = (locationId ?? string.Empty).Trim(); Settings.Default.Save(); } + + public static string InvoiceExportPath + { + get { return Settings.Default.InvoiceExportPath ?? string.Empty; } + } + + public static void SetInvoiceExportPath(string path) + { + Settings.Default.InvoiceExportPath = (path ?? string.Empty).Trim(); + Settings.Default.Save(); + } } } diff --git a/src/ElectronicPharmacy/Classes/InvoiceExportService.cs b/src/ElectronicPharmacy/Classes/InvoiceExportService.cs new file mode 100644 index 0000000..39025fe --- /dev/null +++ b/src/ElectronicPharmacy/Classes/InvoiceExportService.cs @@ -0,0 +1,194 @@ +using System; +using System.Globalization; +using System.IO; +using System.Text; +using System.Windows.Forms; +using System.Data.SQLite; + +namespace Электронная_Фармация.Classes +{ + /// + /// Exports a local invoice (header + items) to a CSV file in the configured folder. + /// + public sealed class InvoiceExportService + { + public string ExportInvoice(string invoiceId, IWin32Window owner = null) + { + if (string.IsNullOrWhiteSpace(invoiceId)) + { + throw new ArgumentException("Не указана накладная для экспорта.", nameof(invoiceId)); + } + + var folder = ResolveExportFolder(owner); + if (string.IsNullOrWhiteSpace(folder)) + { + throw new InvalidOperationException("Не указана папка для экспорта накладных."); + } + + if (!Directory.Exists(folder)) + { + Directory.CreateDirectory(folder); + } + + InvoiceHeader header; + using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + con.Open(); + header = LoadHeader(con, invoiceId); + var itemsSql = @" +select + ifnull([GoodCode], '') as [GoodCode], + ifnull([Good], '') as [Good], + ifnull([ProducerName], '') as [ProducerName], + ifnull([GoodsGount], 0) as [Qty], + ifnull([PriceSupplierWithNDS], ifnull([PriceSupplierWithoutNDS], 0)) as [Price], + ifnull([SumWithNDS], ifnull([SumWithoutNDS], 0)) as [Sum], + ifnull([BestBefore], '') as [BestBefore], + ifnull([Serial], '') as [Serial] +from [InvoiceItem] +where [idInvoice] = @invoiceId +order by [Good];"; + + var fileName = BuildFileName(header); + var filePath = Path.Combine(folder, fileName); + + var sb = new StringBuilder(); + sb.AppendLine("Номер накладной;Дата;Поставщик;Грузополучатель;Номер заказа;Сумма"); + sb.AppendLine(string.Join(";", + Csv(header.Number), + Csv(header.Date), + Csv(header.Supplier), + Csv(header.Consignee), + Csv(header.OrderNumber), + Csv(header.Sum))); + sb.AppendLine(); + sb.AppendLine("Код товара;Товар;Производитель;Количество;Цена;Сумма;Срок годности;Серия"); + + using (var cmd = new SQLiteCommand(itemsSql, con)) + { + cmd.Parameters.AddWithValue("@invoiceId", invoiceId); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + sb.AppendLine(string.Join(";", + Csv(reader["GoodCode"]), + Csv(reader["Good"]), + Csv(reader["ProducerName"]), + Csv(reader["Qty"]), + Csv(reader["Price"]), + Csv(reader["Sum"]), + Csv(reader["BestBefore"]), + Csv(reader["Serial"]))); + } + } + } + + // UTF-8 BOM — Excel корректно открывает кириллицу. + File.WriteAllText(filePath, sb.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)); + AppDebugLog.Info("InvoiceExport", $"Накладная {header.Number} экспортирована: {filePath}"); + return filePath; + } + } + + public static string ResolveExportFolder(IWin32Window owner = null) + { + var folder = AppConfig.InvoiceExportPath; + if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder)) + { + return folder; + } + + using (var dialog = new FolderBrowserDialog()) + { + dialog.Description = "Выберите папку для экспорта накладных"; + dialog.ShowNewFolderButton = true; + if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder)) + { + dialog.SelectedPath = folder; + } + + if (dialog.ShowDialog(owner) != DialogResult.OK || string.IsNullOrWhiteSpace(dialog.SelectedPath)) + { + return string.Empty; + } + + AppConfig.SetInvoiceExportPath(dialog.SelectedPath); + return dialog.SelectedPath; + } + } + + private static InvoiceHeader LoadHeader(SQLiteConnection con, string invoiceId) + { + using (var cmd = new SQLiteCommand(@" +select + ifnull([InvoiceNumber], '') as [Number], + ifnull([InvoiceDate], '') as [Date], + ifnull([SupplierName], '') as [Supplier], + ifnull([ConsigneesName], '') as [Consignee], + ifnull([SourceOrderNumber], '') as [OrderNumber], + ifnull([InvoiceSum], ifnull([SumWithNDS], 0)) as [Sum] +from [Invoice] +where [idInvoice] = @invoiceId +limit 1;", con)) + { + cmd.Parameters.AddWithValue("@invoiceId", invoiceId); + using (var reader = cmd.ExecuteReader()) + { + if (!reader.Read()) + { + throw new InvalidOperationException("Накладная не найдена."); + } + + return new InvoiceHeader + { + Number = reader["Number"]?.ToString() ?? string.Empty, + Date = reader["Date"]?.ToString() ?? string.Empty, + Supplier = reader["Supplier"]?.ToString() ?? string.Empty, + Consignee = reader["Consignee"]?.ToString() ?? string.Empty, + OrderNumber = reader["OrderNumber"]?.ToString() ?? string.Empty, + Sum = reader["Sum"]?.ToString() ?? "0" + }; + } + } + } + + private static string BuildFileName(InvoiceHeader header) + { + var number = SanitizeFileName(string.IsNullOrWhiteSpace(header.Number) ? "invoice" : header.Number); + var stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture); + return $"Накладная_{number}_{stamp}.csv"; + } + + private static string SanitizeFileName(string value) + { + foreach (var c in Path.GetInvalidFileNameChars()) + { + value = value.Replace(c, '_'); + } + + return value.Trim(); + } + + private static string Csv(object value) + { + var text = value?.ToString() ?? string.Empty; + if (text.IndexOfAny(new[] { ';', '"', '\r', '\n' }) >= 0) + { + return "\"" + text.Replace("\"", "\"\"") + "\""; + } + + return text; + } + + private sealed class InvoiceHeader + { + public string Number { get; set; } + public string Date { get; set; } + public string Supplier { get; set; } + public string Consignee { get; set; } + public string OrderNumber { get; set; } + public string Sum { get; set; } + } + } +} diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj index aace520..fd4daf9 100644 --- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj +++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj @@ -106,6 +106,7 @@ + diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs index b9a93d7..3447438 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs @@ -40,6 +40,9 @@ this.label3 = new System.Windows.Forms.Label(); this.label5 = new System.Windows.Forms.Label(); this.txtApiUrl = new Elfisa.UI.Controls.ModernTextBox(); + this.labelExportPath = new System.Windows.Forms.Label(); + this.txtInvoiceExportPath = new Elfisa.UI.Controls.ModernTextBox(); + this.btnBrowseExportPath = new Elfisa.UI.Controls.ModernButton(); this.panel1.SuspendLayout(); this.SuspendLayout(); // @@ -79,9 +82,55 @@ this.txtPassword.Size = new System.Drawing.Size(375, 34); this.txtPassword.TabIndex = 3; // + // label5 + // + this.label5.AutoSize = true; + this.label5.Font = new System.Drawing.Font("Segoe UI", 12F); + 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; + this.label5.Text = "URL API"; + // + // txtApiUrl + // + this.txtApiUrl.Font = new System.Drawing.Font("Segoe UI", 12F); + 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; + // + // labelExportPath + // + this.labelExportPath.AutoSize = true; + this.labelExportPath.Font = new System.Drawing.Font("Segoe UI", 12F); + this.labelExportPath.Location = new System.Drawing.Point(23, 272); + this.labelExportPath.Name = "labelExportPath"; + this.labelExportPath.Size = new System.Drawing.Size(280, 28); + this.labelExportPath.TabIndex = 13; + this.labelExportPath.Text = "Папка экспорта накладных"; + // + // txtInvoiceExportPath + // + this.txtInvoiceExportPath.Font = new System.Drawing.Font("Segoe UI", 10F); + this.txtInvoiceExportPath.Location = new System.Drawing.Point(28, 304); + this.txtInvoiceExportPath.Name = "txtInvoiceExportPath"; + this.txtInvoiceExportPath.Size = new System.Drawing.Size(320, 30); + this.txtInvoiceExportPath.TabIndex = 14; + // + // btnBrowseExportPath + // + this.btnBrowseExportPath.Font = new System.Drawing.Font("Segoe UI Semibold", 10F, System.Drawing.FontStyle.Bold); + this.btnBrowseExportPath.Location = new System.Drawing.Point(354, 304); + this.btnBrowseExportPath.Name = "btnBrowseExportPath"; + this.btnBrowseExportPath.Size = new System.Drawing.Size(49, 30); + this.btnBrowseExportPath.TabIndex = 15; + this.btnBrowseExportPath.Text = "..."; + this.btnBrowseExportPath.Click += new System.EventHandler(this.btnBrowseExportPath_Click); + // // btnConfirmRegistration // - this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 300); + this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 356); this.btnConfirmRegistration.Name = "btnConfirmRegistration"; this.btnConfirmRegistration.Size = new System.Drawing.Size(215, 43); this.btnConfirmRegistration.TabIndex = 5; @@ -90,7 +139,7 @@ // // btnCloseThis // - this.btnCloseThis.Location = new System.Drawing.Point(28, 300); + this.btnCloseThis.Location = new System.Drawing.Point(28, 356); this.btnCloseThis.Name = "btnCloseThis"; this.btnCloseThis.Size = new System.Drawing.Size(125, 43); this.btnCloseThis.TabIndex = 6; @@ -102,7 +151,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, 260); + this.checkTokenGained.Location = new System.Drawing.Point(28, 420); this.checkTokenGained.Name = "checkTokenGained"; this.checkTokenGained.Size = new System.Drawing.Size(171, 32); this.checkTokenGained.TabIndex = 7; @@ -133,29 +182,14 @@ this.label3.Text = "АВТОРИЗАЦИЯ"; this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleRight; // - // label5 - // - this.label5.AutoSize = true; - this.label5.Font = new System.Drawing.Font("Segoe UI", 12F); - 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; - this.label5.Text = "URL API"; - // - // txtApiUrl - // - this.txtApiUrl.Font = new System.Drawing.Font("Segoe UI", 12F); - 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; - // // HF_Registration // this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(436, 360); + this.ClientSize = new System.Drawing.Size(436, 420); + this.Controls.Add(this.btnBrowseExportPath); + this.Controls.Add(this.txtInvoiceExportPath); + this.Controls.Add(this.labelExportPath); this.Controls.Add(this.txtApiUrl); this.Controls.Add(this.label5); this.Controls.Add(this.panel1); @@ -192,5 +226,8 @@ private System.Windows.Forms.Label label3; private System.Windows.Forms.Label label5; private Elfisa.UI.Controls.ModernTextBox txtApiUrl; + private System.Windows.Forms.Label labelExportPath; + private Elfisa.UI.Controls.ModernTextBox txtInvoiceExportPath; + private Elfisa.UI.Controls.ModernButton btnBrowseExportPath; } } diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs index 8069c1f..74cc942 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs @@ -25,6 +25,27 @@ namespace Электронная_Фармация.HelpForms Close(); } + private void btnBrowseExportPath_Click(object sender, EventArgs e) + { + using (var dialog = new FolderBrowserDialog()) + { + dialog.Description = "Выберите папку для экспорта накладных"; + dialog.ShowNewFolderButton = true; + if (!string.IsNullOrWhiteSpace(txtInvoiceExportPath.Text) + && System.IO.Directory.Exists(txtInvoiceExportPath.Text.Trim())) + { + dialog.SelectedPath = txtInvoiceExportPath.Text.Trim(); + } + + if (dialog.ShowDialog(this) == DialogResult.OK) + { + txtInvoiceExportPath.Text = dialog.SelectedPath; + AppConfig.SetInvoiceExportPath(dialog.SelectedPath); + ToastNotification.ShowSuccess("Папка экспорта накладных сохранена"); + } + } + } + private async void btnConfirmRegistration_Click(object sender, EventArgs e) { btnConfirmRegistration.Enabled = false; @@ -39,6 +60,7 @@ namespace Электронная_Фармация.HelpForms // на старый сохранённый/дефолтный адрес, а не на введённый пользователем. string apiUrl = txtApiUrl.Text.Trim(); Settings.Default.ApiBaseUrl = apiUrl; + AppConfig.SetInvoiceExportPath(txtInvoiceExportPath.Text); Settings.Default.Save(); LoginRequest loginRequest = new LoginRequest(); @@ -56,6 +78,7 @@ namespace Электронная_Фармация.HelpForms Settings.Default.stringPassword = strPassword; Settings.Default.stringToken = token; Settings.Default.ApiBaseUrl = client.BaseUrl; + AppConfig.SetInvoiceExportPath(txtInvoiceExportPath.Text); Settings.Default.Save(); AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}"); @@ -104,6 +127,7 @@ namespace Электронная_Фармация.HelpForms txtApiUrl.Text = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl) ? AppConfig.DefaultApiBaseUrl : Settings.Default.ApiBaseUrl; + txtInvoiceExportPath.Text = AppConfig.InvoiceExportPath; checkTokenGained.Checked = doWeHaveAToken.Length > 0; } diff --git a/src/ElectronicPharmacy/Properties/Settings.Designer.cs b/src/ElectronicPharmacy/Properties/Settings.Designer.cs index c582df8..61c34aa 100644 --- a/src/ElectronicPharmacy/Properties/Settings.Designer.cs +++ b/src/ElectronicPharmacy/Properties/Settings.Designer.cs @@ -119,5 +119,17 @@ namespace Электронная_Фармация.Properties { } } + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string InvoiceExportPath { + get { + return ((string)(this["InvoiceExportPath"])); + } + set { + this["InvoiceExportPath"] = value; + } + } + } } diff --git a/src/ElectronicPharmacy/Properties/Settings.settings b/src/ElectronicPharmacy/Properties/Settings.settings index b2c624d..b4863ca 100644 --- a/src/ElectronicPharmacy/Properties/Settings.settings +++ b/src/ElectronicPharmacy/Properties/Settings.settings @@ -26,5 +26,8 @@ False + + + \ No newline at end of file diff --git a/src/ElectronicPharmacy/UserControls/UCInvoices.cs b/src/ElectronicPharmacy/UserControls/UCInvoices.cs index 05fb61a..ba51fdc 100644 --- a/src/ElectronicPharmacy/UserControls/UCInvoices.cs +++ b/src/ElectronicPharmacy/UserControls/UCInvoices.cs @@ -406,7 +406,7 @@ where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)"; return; } - _exportInvoiceMenuItem = new ToolStripMenuItem("Экспорт"); + _exportInvoiceMenuItem = new ToolStripMenuItem("Экспортировать в папку"); _exportInvoiceMenuItem.Click += (_, __) => ExportSelectedInvoice(); _invoiceMenu = new ContextMenuStrip(); @@ -547,11 +547,27 @@ where ii.idInvoice = @invoiceId;"; return; } - var invoiceNumber = dgvInvoice.SelectedRows[0].Cells["Номер"]?.Value?.ToString() ?? string.Empty; - ToastNotification.ShowCustom( - $"Экспорт накладной {invoiceNumber} будет добавлен после согласования формата.", - Color.SteelBlue, - Color.White); + var row = dgvInvoice.SelectedRows[0]; + var invoiceId = row.Cells["ИД накладной"]?.Value?.ToString() ?? string.Empty; + var invoiceNumber = row.Cells["Номер"]?.Value?.ToString() ?? string.Empty; + + if (string.IsNullOrWhiteSpace(invoiceId)) + { + ToastNotification.ShowCustom("Не удалось определить накладную для экспорта.", Color.DarkOrange, Color.White); + return; + } + + try + { + var exportService = new InvoiceExportService(); + var filePath = exportService.ExportInvoice(invoiceId, FindForm()); + ToastNotification.ShowSuccess($"Накладная {invoiceNumber} сохранена:\n{filePath}"); + } + catch (Exception ex) + { + AppDebugLog.Error("Invoices", $"Ошибка экспорта накладной {invoiceNumber}", ex); + ToastNotification.ShowError($"Не удалось экспортировать накладную: {ex.Message}"); + } } private void txtInvoiceNumber_KeyPress(object sender, KeyPressEventArgs e) From 1f6f442b2122ebfeca1271f8785a36c58f68af4b Mon Sep 17 00:00:00 2001 From: Magomed Date: Sat, 18 Jul 2026 15:45:43 +0300 Subject: [PATCH 3/3] Show real 0-100% download progress from transferred bytes and saved rows. Co-authored-by: Cursor --- src/ElectronicPharmacy/Classes/ApiClient.cs | 73 ++++++- .../HF_DownloadDataFromServer.Designer.cs | 4 +- .../HelpForms/HF_DownloadDataFromServer.cs | 189 +++++++++++------- 3 files changed, 193 insertions(+), 73 deletions(-) diff --git a/src/ElectronicPharmacy/Classes/ApiClient.cs b/src/ElectronicPharmacy/Classes/ApiClient.cs index 567852f..1706e0b 100644 --- a/src/ElectronicPharmacy/Classes/ApiClient.cs +++ b/src/ElectronicPharmacy/Classes/ApiClient.cs @@ -98,7 +98,13 @@ namespace Электронная_Фармация.Classes public bool IsAuthenticated => !string.IsNullOrEmpty(_token); - public async Task GetPriceSummaryAsync(string supplierId = null, string regionId = null, string q = null, int? limit = null, int? offset = null) + public async Task GetPriceSummaryAsync( + string supplierId = null, + string regionId = null, + string q = null, + int? limit = null, + int? offset = null, + IProgress transferProgress = null) { EnsureAuthenticated(); @@ -130,7 +136,10 @@ namespace Электронная_Фармация.Classes path += "?" + string.Join("&", queryParams); } - var responseBody = await SendAuthorizedGetAsync(path, "загрузку прайса"); + var responseBody = transferProgress == null + ? await SendAuthorizedGetAsync(path, "загрузку прайса") + : await SendAuthorizedGetWithProgressAsync(path, "загрузку прайса", transferProgress); + try { var summary = JsonSerializer.Deserialize(responseBody, JsonOptions); @@ -174,6 +183,14 @@ namespace Электронная_Фармация.Classes } private async Task SendAuthorizedGetAsync(string path, string operationName) + { + return await SendAuthorizedGetWithProgressAsync(path, operationName, null); + } + + private async Task SendAuthorizedGetWithProgressAsync( + string path, + string operationName, + IProgress transferProgress) { var fullUrl = BuildFullUrl(path); var stopwatch = Stopwatch.StartNew(); @@ -185,8 +202,8 @@ namespace Электронная_Фармация.Classes using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl)) { httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); - var response = await _httpClient.SendAsync(httpRequest); - var responseBody = await response.Content.ReadAsStringAsync(); + var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead); + var responseBody = await ReadContentWithProgressAsync(response, transferProgress); stopwatch.Stop(); AppDebugLog.ApiResponse("GET", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody); @@ -243,6 +260,29 @@ namespace Электронная_Фармация.Classes } } + private static async Task ReadContentWithProgressAsync( + HttpResponseMessage response, + IProgress transferProgress) + { + var totalBytes = response.Content.Headers.ContentLength; + using (var stream = await response.Content.ReadAsStreamAsync()) + using (var memory = new System.IO.MemoryStream()) + { + var buffer = new byte[81920]; + long received = 0; + int read; + while ((read = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) + { + memory.Write(buffer, 0, read); + received += read; + transferProgress?.Report(new HttpTransferProgress(received, totalBytes)); + } + + transferProgress?.Report(new HttpTransferProgress(received, totalBytes ?? received)); + return Encoding.UTF8.GetString(memory.ToArray()); + } + } + private string ParseLoginResponse(HttpResponseMessage response, string responseBody) { if (response.IsSuccessStatusCode) @@ -333,4 +373,29 @@ namespace Электронная_Фармация.Classes public string InvoiceSum { get; set; } public string RefuseSum { get; set; } } + + public sealed class HttpTransferProgress + { + public HttpTransferProgress(long bytesReceived, long? totalBytes) + { + BytesReceived = bytesReceived; + TotalBytes = totalBytes; + } + + public long BytesReceived { get; } + public long? TotalBytes { get; } + + public int Percent + { + get + { + if (!TotalBytes.HasValue || TotalBytes.Value <= 0) + { + return 0; + } + + return (int)Math.Max(0, Math.Min(100, (100.0 * BytesReceived) / TotalBytes.Value)); + } + } + } } diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs index d47487a..e9c9758 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs @@ -52,10 +52,10 @@ // progressDownload // this.progressDownload.Location = new System.Drawing.Point(25, 52); - this.progressDownload.MarqueeAnimationSpeed = 30; this.progressDownload.Name = "progressDownload"; this.progressDownload.Size = new System.Drawing.Size(630, 22); - this.progressDownload.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.progressDownload.Style = System.Windows.Forms.ProgressBarStyle.Continuous; + this.progressDownload.Maximum = 100; this.progressDownload.TabIndex = 6; // // lblProgress diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs index 95b4dae..3d8b23b 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs @@ -12,6 +12,11 @@ namespace Электронная_Фармация.HelpForms { public partial class HF_DownloadDataFromServer : Form { + // Доли общего прогресса 0..100. + private const int DownloadPhaseEnd = 45; + private const int BuildPhaseEnd = 55; + private const int SavePhaseEnd = 100; + public HF_DownloadDataFromServer() { InitializeComponent(); @@ -30,7 +35,7 @@ namespace Электронная_Фармация.HelpForms rtxtDebug.Visible = true; rtxtDebug.ReadOnly = true; _isDownloading = true; - SetMarqueeProgress("Подключение к серверу..."); + SetOverallProgress(0, "Подключение к серверу..."); var client = new ApiClient(); AppendStatus($"Сервер: {client.BaseUrl}"); @@ -39,43 +44,58 @@ namespace Электронная_Фармация.HelpForms try { - SetMarqueeProgress("Авторизация..."); + SetOverallProgress(1, "Авторизация..."); await EnsureAuthenticatedAsync(client); - SetMarqueeProgress("Загрузка прайса с сервера..."); - AppendStatus("Загружаю сводный прайс (постранично, весь)..."); + SetOverallProgress(3, "Загрузка прайса с сервера..."); + AppendStatus("Загружаю сводный прайс..."); var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client); - AppendStatus($"Получено {allSuppliersSummary.Summary?.Count ?? 0} позиций"); + int totalItems = allSuppliersSummary.Summary?.Count ?? 0; + AppendStatus($"Получено {totalItems} позиций"); + SetOverallProgress(DownloadPhaseEnd, $"Получено {totalItems} / {totalItems} позиций (100%)"); - SetMarqueeProgress("Подготовка таблицы..."); - var tablePrice = await Task.Run(() => BuildPriceTable(allSuppliersSummary)); + SetOverallProgress(DownloadPhaseEnd, "Подготовка таблицы..."); + var buildProgress = new Progress(p => + { + int phasePercent = MapPhasePercent(p.Current, p.Total, DownloadPhaseEnd, BuildPhaseEnd); + SetOverallProgress( + phasePercent, + $"Обработка: {p.Current} / {p.Total} ({PercentOf(p.Current, p.Total)}%)"); + }); + var tablePrice = await Task.Run(() => BuildPriceTable(allSuppliersSummary, buildProgress)); - SetDeterminateProgress(0, Math.Max(tablePrice.Rows.Count, 1), "Сохранение в локальную базу..."); - var progress = new Progress(ReportSaveProgress); - await Task.Run(() => SavePriceTable(tablePrice, progress)); + SetOverallProgress(BuildPhaseEnd, "Сохранение в локальную базу..."); + var saveProgress = new Progress(p => + { + int phasePercent = MapPhasePercent(p.Current, p.Total, BuildPhaseEnd, SavePhaseEnd); + SetOverallProgress( + phasePercent, + $"Сохранение: {p.Current} / {p.Total} ({PercentOf(p.Current, p.Total)}%)"); + }); + await Task.Run(() => SavePriceTable(tablePrice, saveProgress)); - SetDeterminateProgress(100, 100, "Готово"); + SetOverallProgress(100, $"Готово: {tablePrice.Rows.Count} / {tablePrice.Rows.Count} (100%)"); DialogResult = DialogResult.OK; AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу."); ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций"); } catch (UnauthorizedAccessException ex) { - SetMarqueeProgress("Ошибка авторизации"); + SetOverallProgress(0, "Ошибка авторизации"); AppendStatus($"Ошибка авторизации: {ex.Message}"); AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}"); } catch (HttpRequestException ex) { - SetMarqueeProgress("Ошибка сети"); + SetOverallProgress(0, "Ошибка сети"); AppendStatus($"Ошибка сети: {ex.Message}"); AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка сети: {ex.Message}"); } catch (Exception ex) { - SetMarqueeProgress("Ошибка загрузки"); + SetOverallProgress(0, "Ошибка загрузки"); AppendStatus($"Ошибка загрузки прайса: {ex.Message}"); AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex); ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}"); @@ -114,45 +134,51 @@ namespace Электронная_Фармация.HelpForms AppendStatus("Авторизация успешно пройдена"); } - // Тянем весь прайс постранично: сервер отдаёт максимум 5000 за запрос, - // поэтому идём offset'ом, пока страница не окажется неполной. private async Task GetFullPriceSummaryPagedAsync(ApiClient client) { var regionId = string.IsNullOrWhiteSpace(Settings.Default.RegionId) ? null : Settings.Default.RegionId; - // Тянем весь прайс одним запросом: постраничный OFFSET на тяжёлом - // сводном запросе валит сервер на глубоких страницах. Один запрос = - // один расчёт выборки. Страховка-цикл остаётся на случай, если позиций - // окажется больше pageSize. + // Один крупный запрос (OFFSET на больших страницах валит сервер). const int pageSize = 200000; const int maxPages = 100; var combined = new PriceSummaryResponse { Summary = new List() }; + var transferProgress = new Progress(ReportTransferProgress); for (int page = 0; page < maxPages; page++) { int offset = page * pageSize; - SetMarqueeProgress(page == 0 - ? "Ожидание ответа сервера..." - : $"Загрузка страницы {page + 1}..."); + SetOverallProgress( + Math.Min(DownloadPhaseEnd - 1, 3 + page), + page == 0 ? "Скачивание прайса с сервера..." : $"Скачивание страницы {page + 1}..."); PriceSummaryResponse chunk; try { - chunk = await client.GetPriceSummaryAsync(supplierId: null, regionId: regionId, limit: pageSize, offset: offset); + chunk = await client.GetPriceSummaryAsync( + supplierId: null, + regionId: regionId, + limit: pageSize, + offset: offset, + transferProgress: transferProgress); } catch (UnauthorizedAccessException) { AppendStatus("Токен недействителен, повторная авторизация..."); - SetMarqueeProgress("Повторная авторизация..."); + SetOverallProgress(2, "Повторная авторизация..."); Settings.Default.stringToken = string.Empty; Settings.Default.Save(); client.SetToken(null); await LoginAndSaveTokenAsync(client); - SetMarqueeProgress("Повторная загрузка прайса..."); - chunk = await client.GetPriceSummaryAsync(supplierId: null, regionId: regionId, limit: pageSize, offset: offset); + SetOverallProgress(3, "Повторная загрузка прайса..."); + chunk = await client.GetPriceSummaryAsync( + supplierId: null, + regionId: regionId, + limit: pageSize, + offset: offset, + transferProgress: transferProgress); } int got = chunk?.Summary?.Count ?? 0; @@ -160,19 +186,37 @@ namespace Электронная_Фармация.HelpForms { combined.Summary.AddRange(chunk.Summary); AppendStatus($"Загружено {combined.Summary.Count} позиций..."); - SetMarqueeProgress($"Получено {combined.Summary.Count} позиций..."); } if (got < pageSize) { - break; // последняя (неполная) страница — дальше данных нет + break; } } return combined; } - private static DataTable BuildPriceTable(PriceSummaryResponse allSuppliersSummary) + private void ReportTransferProgress(HttpTransferProgress transfer) + { + if (transfer.TotalBytes.HasValue && transfer.TotalBytes.Value > 0) + { + int phasePercent = MapPhasePercent(transfer.BytesReceived, transfer.TotalBytes.Value, 3, DownloadPhaseEnd); + SetOverallProgress( + phasePercent, + $"Скачивание: {FormatBytes(transfer.BytesReceived)} / {FormatBytes(transfer.TotalBytes.Value)} ({transfer.Percent}%)"); + } + else + { + SetOverallProgress( + Math.Min(DownloadPhaseEnd - 1, 5), + $"Скачивание: получено {FormatBytes(transfer.BytesReceived)}..."); + } + } + + private static DataTable BuildPriceTable( + PriceSummaryResponse allSuppliersSummary, + IProgress progress = null) { var tablePrice = new DataTable(); tablePrice.Columns.Add("supplier_price_id"); @@ -215,6 +259,8 @@ namespace Электронная_Фармация.HelpForms return tablePrice; } + int total = allSuppliersSummary.Summary.Count; + int current = 0; foreach (var item in allSuppliersSummary.Summary) { var row = tablePrice.NewRow(); @@ -233,12 +279,18 @@ namespace Электронная_Фармация.HelpForms row["TradeName"] = string.IsNullOrWhiteSpace(item.TradeName) ? (object)DBNull.Value : item.TradeName.Trim(); row["Dosage"] = string.IsNullOrWhiteSpace(item.Dosage) ? (object)DBNull.Value : item.Dosage.Trim(); tablePrice.Rows.Add(row); + + current++; + if (progress != null && (current % 500 == 0 || current == total)) + { + progress.Report(new CountProgress(current, total)); + } } return tablePrice; } - private void SavePriceTable(DataTable tablePrice, IProgress progress = null) + private void SavePriceTable(DataTable tablePrice, IProgress progress = null) { const string commandToDelete = "delete from [PriceList]"; const string commandToDeleteTempOrder = "delete from [TempOrderItems]"; @@ -315,7 +367,7 @@ VALUES saved++; if (progress != null && (saved % 250 == 0 || saved == total)) { - progress.Report(new SaveProgress(saved, total)); + progress.Report(new CountProgress(saved, total)); } } @@ -324,21 +376,43 @@ VALUES } } - private void ReportSaveProgress(SaveProgress state) + private static int MapPhasePercent(long current, long total, int phaseStart, int phaseEnd) { - if (IsDisposed) + if (total <= 0) { - return; + return phaseStart; } - int percent = state.Total <= 0 ? 0 : (int)(100.0 * state.Current / state.Total); - SetDeterminateProgress( - percent, - 100, - $"Сохранение в базу: {state.Current} / {state.Total} ({percent}%)"); + double ratio = Math.Max(0.0, Math.Min(1.0, (double)current / total)); + return phaseStart + (int)((phaseEnd - phaseStart) * ratio); } - private void SetMarqueeProgress(string text) + private static int PercentOf(long current, long total) + { + if (total <= 0) + { + return 0; + } + + return (int)Math.Max(0, Math.Min(100, (100.0 * current) / total)); + } + + private static string FormatBytes(long bytes) + { + if (bytes < 1024) + { + return $"{bytes} Б"; + } + + if (bytes < 1024 * 1024) + { + return $"{bytes / 1024.0:0.0} КБ"; + } + + return $"{bytes / (1024.0 * 1024.0):0.00} МБ"; + } + + private void SetOverallProgress(int percent, string text) { if (IsDisposed) { @@ -347,27 +421,7 @@ VALUES if (InvokeRequired) { - BeginInvoke(new Action(SetMarqueeProgress), text); - return; - } - - progressDownload.Style = ProgressBarStyle.Marquee; - progressDownload.MarqueeAnimationSpeed = 30; - lblProgress.Text = text ?? string.Empty; - lblProgress.Refresh(); - progressDownload.Refresh(); - } - - private void SetDeterminateProgress(int value, int maximum, string text) - { - if (IsDisposed) - { - return; - } - - if (InvokeRequired) - { - BeginInvoke(new Action(SetDeterminateProgress), value, maximum, text); + BeginInvoke(new Action(SetOverallProgress), percent, text); return; } @@ -376,9 +430,10 @@ VALUES progressDownload.Style = ProgressBarStyle.Continuous; } - progressDownload.Maximum = Math.Max(maximum, 1); - progressDownload.Value = Math.Max(0, Math.Min(value, progressDownload.Maximum)); - lblProgress.Text = text ?? string.Empty; + progressDownload.Minimum = 0; + progressDownload.Maximum = 100; + progressDownload.Value = Math.Max(0, Math.Min(100, percent)); + lblProgress.Text = $"{Math.Max(0, Math.Min(100, percent))}% — {text}"; lblProgress.Refresh(); progressDownload.Refresh(); } @@ -411,9 +466,9 @@ VALUES Close(); } - private sealed class SaveProgress + private sealed class CountProgress { - public SaveProgress(int current, int total) + public CountProgress(int current, int total) { Current = current; Total = total;